File: //tmp/bewertemich-tracking-loop.js
import { apiFetch } from '../modules/app/api.js';
const visitorKey = 'bm_visitor_id';
const sessionKey = 'bm_session_id';
const inviteAttributionKey = 'bm_invite_attribution';
const sanitizeKey = (value, maxLength = 80) => {
const raw = String(value || '').trim().toLowerCase();
if (!raw) return '';
return raw.replace(/[^a-z0-9:_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, maxLength);
};
const makeId = () => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();
return `bm-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
};
const readStorage = (storage, key) => {
try {
return storage.getItem(key) || '';
} catch {
return '';
}
};
const writeStorage = (storage, key, value) => {
try {
storage.setItem(key, value);
} catch {}
};
export const getVisitorId = () => {
if (typeof window === 'undefined') return '';
const existing = readStorage(window.localStorage, visitorKey);
if (existing) return existing;
const next = makeId();
writeStorage(window.localStorage, visitorKey, next);
return next;
};
export const getSessionId = () => {
if (typeof window === 'undefined') return '';
const existing = readStorage(window.sessionStorage, sessionKey);
if (existing) return existing;
const next = makeId();
writeStorage(window.sessionStorage, sessionKey, next);
return next;
};
export const getStoredInviteAttribution = () => {
if (typeof window === 'undefined') return null;
try {
const raw = window.sessionStorage.getItem(inviteAttributionKey);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
};
export const rememberInviteAttribution = (search = '', photoId = '') => {
if (typeof window === 'undefined') return null;
const params = new URLSearchParams(search || '');
const campaign = sanitizeKey(params.get('src'), 30);
if (campaign !== 'invite') return null;
const payload = {
source: sanitizeKey(params.get('via'), 50) || 'direct',
surface: sanitizeKey(params.get('surface'), 50) || 'share',
sourcePhotoId: String(photoId || '').replace(/[^0-9]+/g, '').slice(0, 20),
capturedAt: new Date().toISOString()
};
try {
window.sessionStorage.setItem(inviteAttributionKey, JSON.stringify(payload));
} catch {}
return payload;
};
export const getUploadEntry = (search = '') => {
const params = new URLSearchParams(search || '');
const directEntry = sanitizeKey(params.get('entry'), 80);
if (directEntry) return directEntry;
const invite = getStoredInviteAttribution();
if (invite?.sourcePhotoId || invite?.source) return 'invite_to_upload';
return 'direct';
};
export const buildUploadPath = (entry = 'direct') => {
const safeEntry = sanitizeKey(entry, 80) || 'direct';
return `/upload?entry=${encodeURIComponent(safeEntry)}`;
};
export const getInviteAttribution = (search = '') => {
const params = new URLSearchParams(search || '');
const campaign = sanitizeKey(params.get('src'), 30);
const source = sanitizeKey(params.get('via'), 50);
const surface = sanitizeKey(params.get('surface'), 50);
return {
isInvite: campaign === 'invite',
source: source || (campaign === 'invite' ? 'direct' : ''),
surface: surface || ''
};
};
export const buildShareUrl = (photoId, { source = 'copy', surface = 'upload_success' } = {}) => {
if (typeof window === 'undefined') return '';
const id = Number(photoId);
if (!Number.isInteger(id) || id <= 0) return '';
const params = new URLSearchParams({ src: 'invite' });
const safeSource = sanitizeKey(source, 50);
const safeSurface = sanitizeKey(surface, 50);
if (safeSource) params.set('via', safeSource);
if (safeSurface) params.set('surface', safeSurface);
return `${window.location.origin}/s/${id}?${params.toString()}`;
};
export const buildComparisonShareUrl = (comparisonId, { source = 'copy', surface = 'compare_page' } = {}) => {
if (typeof window === 'undefined') return '';
const id = Number(comparisonId);
if (!Number.isInteger(id) || id <= 0) return '';
const params = new URLSearchParams({ src: 'invite' });
const safeSource = sanitizeKey(source, 50);
const safeSurface = sanitizeKey(surface, 50);
if (safeSource) params.set('via', safeSource);
if (safeSurface) params.set('surface', safeSurface);
return `${window.location.origin}/s/compare/${id}?${params.toString()}`;
};
const pushMatomo = (eventName, source, surface) => {
try {
const _paq = window._paq;
if (!Array.isArray(_paq)) return;
_paq.push(['trackEvent', 'Growth', eventName, `${source || 'none'}:${surface || 'none'}`]);
} catch {}
};
const postEvent = async (body, transport) => {
if (typeof window === 'undefined') return;
if (transport === 'beacon' && typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
try {
const blob = new Blob([JSON.stringify(body)], { type: 'application/json' });
if (navigator.sendBeacon('/api/public/events', blob)) return;
} catch {}
}
try {
await apiFetch('/api/public/events', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
keepalive: transport === 'beacon'
});
} catch {}
};
export const trackEvent = async (eventName, payload = {}, options = {}) => {
const safeEvent = sanitizeKey(eventName, 80);
if (!safeEvent || typeof window === 'undefined') return;
const source = sanitizeKey(payload.source, 50);
const surface = sanitizeKey(payload.surface, 50);
const entryPoint = sanitizeKey(payload.entryPoint, 80);
pushMatomo(safeEvent, source, surface);
await postEvent(
{
event: safeEvent,
visitorId: getVisitorId(),
sessionId: getSessionId(),
photoId: Number.isInteger(payload.photoId) && payload.photoId > 0 ? payload.photoId : undefined,
source: source || undefined,
surface: surface || undefined,
entryPoint: entryPoint || undefined,
path: `${window.location.pathname}${window.location.search}`,
referrer: document.referrer || '',
meta: payload.meta || undefined
},
options.transport
);
};