212 lines
5.7 KiB
JavaScript
212 lines
5.7 KiB
JavaScript
const WORLD_ANALYTICS_ENDPOINT = '/api/worlds/analytics/events'
|
|
const VISITOR_STORAGE_KEY = 'skinbase:world-analytics-visitor'
|
|
const SOURCE_PARAM = 'world_source'
|
|
const SOURCE_DETAIL_PARAM = 'world_source_detail'
|
|
const IMPRESSION_KEYS = new Set()
|
|
|
|
const ALLOWED_SOURCES = new Set([
|
|
'homepage_spotlight',
|
|
'homepage_worlds_rail',
|
|
'worlds_index',
|
|
'navigation',
|
|
'upload_flow',
|
|
'challenge_page',
|
|
'news_article',
|
|
'profile',
|
|
'direct',
|
|
'unknown',
|
|
])
|
|
|
|
function csrfToken() {
|
|
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
|
}
|
|
|
|
function randomToken() {
|
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
return crypto.randomUUID()
|
|
}
|
|
|
|
return `w-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`
|
|
}
|
|
|
|
function normalizeSourceSurface(value) {
|
|
const normalized = String(value || '').trim()
|
|
return ALLOWED_SOURCES.has(normalized) ? normalized : ''
|
|
}
|
|
|
|
function sanitizeDetail(value) {
|
|
return String(value || '').trim().slice(0, 80)
|
|
}
|
|
|
|
function impressionKey({ worldId, sourceSurface, sourceDetail = '', sectionKey = '' }) {
|
|
return [worldId, sourceSurface, sanitizeDetail(sourceDetail), String(sectionKey || '').trim()].join(':')
|
|
}
|
|
|
|
export function worldAnalyticsVisitorToken() {
|
|
try {
|
|
const existing = window.localStorage?.getItem(VISITOR_STORAGE_KEY)
|
|
if (existing) {
|
|
return existing
|
|
}
|
|
|
|
const next = randomToken()
|
|
window.localStorage?.setItem(VISITOR_STORAGE_KEY, next)
|
|
return next
|
|
} catch {
|
|
return randomToken()
|
|
}
|
|
}
|
|
|
|
export function withWorldSource(url, sourceSurface, sourceDetail = '') {
|
|
if (!url) {
|
|
return url
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(url, window.location.origin)
|
|
if (parsed.origin !== window.location.origin) {
|
|
return url
|
|
}
|
|
|
|
const normalizedSource = normalizeSourceSurface(sourceSurface)
|
|
if (normalizedSource) {
|
|
parsed.searchParams.set(SOURCE_PARAM, normalizedSource)
|
|
}
|
|
|
|
const normalizedDetail = sanitizeDetail(sourceDetail)
|
|
if (normalizedDetail) {
|
|
parsed.searchParams.set(SOURCE_DETAIL_PARAM, normalizedDetail)
|
|
}
|
|
|
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
} catch {
|
|
return url
|
|
}
|
|
}
|
|
|
|
export function resolveWorldLandingSource() {
|
|
try {
|
|
const locationUrl = new URL(window.location.href)
|
|
const explicitSource = normalizeSourceSurface(locationUrl.searchParams.get(SOURCE_PARAM))
|
|
const explicitDetail = sanitizeDetail(locationUrl.searchParams.get(SOURCE_DETAIL_PARAM))
|
|
|
|
if (explicitSource) {
|
|
return {
|
|
sourceSurface: explicitSource,
|
|
sourceDetail: explicitDetail,
|
|
}
|
|
}
|
|
|
|
if (!document.referrer) {
|
|
return { sourceSurface: 'direct', sourceDetail: '' }
|
|
}
|
|
|
|
const referrer = new URL(document.referrer)
|
|
if (referrer.origin !== window.location.origin) {
|
|
return { sourceSurface: 'unknown', sourceDetail: 'external_referrer' }
|
|
}
|
|
|
|
const path = referrer.pathname || '/'
|
|
|
|
if (path === '/') {
|
|
return { sourceSurface: 'homepage_spotlight', sourceDetail: 'referrer' }
|
|
}
|
|
|
|
if (path === '/worlds') {
|
|
return { sourceSurface: 'worlds_index', sourceDetail: 'referrer' }
|
|
}
|
|
|
|
if (/^\/groups\/[^/]+\/challenges\//.test(path)) {
|
|
return { sourceSurface: 'challenge_page', sourceDetail: 'referrer' }
|
|
}
|
|
|
|
if (path.startsWith('/upload') || path.startsWith('/studio/artworks')) {
|
|
return { sourceSurface: 'upload_flow', sourceDetail: 'referrer' }
|
|
}
|
|
|
|
if (path.startsWith('/news') || path.startsWith('/stories')) {
|
|
return { sourceSurface: 'news_article', sourceDetail: 'referrer' }
|
|
}
|
|
|
|
if (path.startsWith('/@') || path.startsWith('/profile')) {
|
|
return { sourceSurface: 'profile', sourceDetail: 'referrer' }
|
|
}
|
|
} catch {
|
|
return { sourceSurface: 'unknown', sourceDetail: '' }
|
|
}
|
|
|
|
return { sourceSurface: 'unknown', sourceDetail: '' }
|
|
}
|
|
|
|
export async function trackWorldAnalytics(eventType, payload = {}) {
|
|
try {
|
|
if (!eventType || !payload.world_id) {
|
|
return
|
|
}
|
|
|
|
await fetch(WORLD_ANALYTICS_ENDPOINT, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
keepalive: true,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken(),
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
body: JSON.stringify({
|
|
event_type: eventType,
|
|
visitor_token: worldAnalyticsVisitorToken(),
|
|
...payload,
|
|
}),
|
|
})
|
|
} catch {
|
|
// Best-effort analytics only.
|
|
}
|
|
}
|
|
|
|
export function trackWorldSourceClick({ worldId, worldTitle = '', sourceSurface = '', sourceDetail = '' }) {
|
|
const normalizedSource = normalizeSourceSurface(sourceSurface)
|
|
if (!worldId || !normalizedSource) {
|
|
return
|
|
}
|
|
|
|
trackWorldAnalytics('world_source_clicked', {
|
|
world_id: worldId,
|
|
source_surface: normalizedSource,
|
|
source_detail: sanitizeDetail(sourceDetail),
|
|
entity_type: 'world',
|
|
entity_id: worldId,
|
|
entity_title: worldTitle,
|
|
})
|
|
}
|
|
|
|
export function trackWorldSourceImpression({
|
|
worldId,
|
|
worldTitle = '',
|
|
sourceSurface = '',
|
|
sourceDetail = '',
|
|
sectionKey = '',
|
|
}) {
|
|
const normalizedSource = normalizeSourceSurface(sourceSurface)
|
|
if (!worldId || !normalizedSource) {
|
|
return
|
|
}
|
|
|
|
const key = impressionKey({ worldId, sourceSurface: normalizedSource, sourceDetail, sectionKey })
|
|
if (IMPRESSION_KEYS.has(key)) {
|
|
return
|
|
}
|
|
|
|
IMPRESSION_KEYS.add(key)
|
|
|
|
trackWorldAnalytics('world_source_impression', {
|
|
world_id: worldId,
|
|
source_surface: normalizedSource,
|
|
source_detail: sanitizeDetail(sourceDetail),
|
|
section_key: String(sectionKey || '').trim().slice(0, 80),
|
|
entity_type: 'world',
|
|
entity_id: worldId,
|
|
entity_title: worldTitle,
|
|
})
|
|
} |