mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-12 08:16:09 +00:00
- demo_opened (une fois par chargement, visiteur sans compte) et demo_interaction (personne, média, recherche, édition, création d'arbre ; une fois par type, sans contenu) - Source d'arrivée gardée sur l'appareil (?source=hn) et jointe à account_screen_viewed et account_registered ; la landing transmet la vraie source au CTA et à l'iframe de démo au lieu de source=landing - Plus de remise à zéro PostHog à chaque ouverture anonyme (effaçait le visiteur de la landing), seulement à la déconnexion - Landing : un seul événement signup_cta_clicked (hero_cta_clicked retiré, plus utilisé par le funnel), envoyé avant la navigation sans sendBeacon (bloqué par Brave) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
3.9 KiB
JavaScript
124 lines
3.9 KiB
JavaScript
/**
|
|
* PostHog analytics wrapper.
|
|
* Activate by setting VITE_POSTHOG_KEY in your .env file.
|
|
* Events are silently dropped when the key is absent.
|
|
*/
|
|
|
|
const KEY = import.meta.env.VITE_POSTHOG_KEY
|
|
let ph = null
|
|
let initStarted = false
|
|
let enabled = Boolean(KEY)
|
|
const queuedEvents = []
|
|
const MAX_QUEUED_EVENTS = 100
|
|
|
|
// Une adresse d'arbre (/arbre/<slug>) peut arriver en referrer : elle ne part jamais chez PostHog.
|
|
// Même règle que frontend/src/utils/analytics.js.
|
|
function scrubTreeSlug(value) {
|
|
if (typeof value === 'string') {
|
|
return value
|
|
.replace(/\/arbre\/[^/?#\s]+/g, '/arbre/:slug')
|
|
.replace(/%2Farbre%2F(?:(?!%2F|%3F|%23)[^&#\s])+/gi, '%2Farbre%2F%3Aslug')
|
|
}
|
|
if (Array.isArray(value)) return value.map(scrubTreeSlug)
|
|
if (value && typeof value === 'object' && !(value instanceof Date)) {
|
|
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, scrubTreeSlug(v)]))
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function scrubAnalyticsEvent(event) {
|
|
if (!event) return event
|
|
const next = { ...event }
|
|
for (const key of ['properties', '$set', '$set_once']) {
|
|
if (next[key]) next[key] = scrubTreeSlug(next[key])
|
|
}
|
|
return next
|
|
}
|
|
|
|
function flushQueue() {
|
|
if (!ph || typeof ph.capture !== 'function') return
|
|
while (queuedEvents.length > 0) {
|
|
const next = queuedEvents.shift()
|
|
ph.capture(next.event, next.props)
|
|
}
|
|
}
|
|
|
|
export const analytics = {
|
|
init() {
|
|
if (initStarted) return
|
|
initStarted = true
|
|
|
|
if (!KEY) return
|
|
|
|
import('posthog-js')
|
|
.then(({ default: posthog }) => {
|
|
posthog.init(KEY, {
|
|
api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com',
|
|
cross_subdomain_cookie: true,
|
|
defaults: '2026-01-30',
|
|
person_profiles: 'identified_only',
|
|
autocapture: false,
|
|
capture_pageview: false,
|
|
disable_session_recording: true,
|
|
// Aucun feature flag dans le code. Sans ça, /flags recevrait les propriétés de personne
|
|
// ($initial_current_url...), que before_send ne voit pas.
|
|
advanced_disable_flags: true,
|
|
// Même verrouillage que l'app (frontend/src/utils/analytics.js) : rien d'activable depuis le tableau
|
|
// de bord PostHog, aucun script externe
|
|
capture_dead_clicks: false,
|
|
capture_heatmaps: false,
|
|
capture_exceptions: false,
|
|
capture_performance: false,
|
|
rageclick: false,
|
|
disable_surveys: true,
|
|
disable_product_tours: true,
|
|
disable_web_experiments: true,
|
|
disable_external_dependency_loading: true,
|
|
before_send: scrubAnalyticsEvent,
|
|
})
|
|
ph = posthog
|
|
flushQueue()
|
|
})
|
|
.catch(() => {
|
|
enabled = false
|
|
queuedEvents.length = 0
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Pour un clic qui quitte la page : l'événement part tout de suite (pas de file d'attente), puis on
|
|
* laisse 350 ms à la requête avant de naviguer. Pas de sendBeacon : Brave bloque ces requêtes « ping »
|
|
* (vu le 05/10, ERR_BLOCKED_BY_CLIENT). Si PostHog n'est pas encore chargé (init différée jusqu'à 3 s),
|
|
* on le charge et on attend au plus 800 ms avant de partir quand même.
|
|
*/
|
|
captureThenNavigate(event, props, url) {
|
|
const go = () => window.location.assign(url)
|
|
if (!enabled) return go()
|
|
const send = () => {
|
|
try { ph.capture(event, props, { send_instantly: true }) } catch { /* on part quand même */ }
|
|
setTimeout(go, 350)
|
|
}
|
|
if (ph) return send()
|
|
this.init()
|
|
const startedAt = Date.now()
|
|
const wait = () => {
|
|
if (ph) return send()
|
|
if (!enabled || Date.now() - startedAt > 800) return go()
|
|
setTimeout(wait, 50)
|
|
}
|
|
wait()
|
|
},
|
|
|
|
capture(event, props = {}) {
|
|
if (!event) return
|
|
if (ph && typeof ph.capture === 'function') {
|
|
ph.capture(event, props)
|
|
return
|
|
}
|
|
|
|
if (!enabled) return
|
|
if (queuedEvents.length >= MAX_QUEUED_EVENTS) return
|
|
queuedEvents.push({ event, props })
|
|
},
|
|
}
|