export type RybbitEvent = { type?: string; timestamp?: string; pathname?: string; path?: string; url?: string; event_name?: string; name?: string; properties?: Record; }; export type AuditRybbitSummary = { opened: boolean; viewCount: number; lastView: string | null; ctaClicks: number; websiteLinkClicks: number; deviceTypes: string[]; }; type FetchLike = ( input: string | URL, init?: RequestInit, ) => Promise>; export function buildRybbitEventsUrl(input: { apiUrl: string; siteId: string; startDate?: string; endDate?: string; }) { const base = input.apiUrl.endsWith("/") ? input.apiUrl : `${input.apiUrl}/`; const url = new URL(`api/sites/${encodeURIComponent(input.siteId)}/events`, base); if (input.startDate) { url.searchParams.set("start_date", input.startDate); } if (input.endDate) { url.searchParams.set("end_date", input.endDate); } return url; } function eventPath(event: RybbitEvent) { const propertyPath = typeof event.properties?.pathname === "string" ? event.properties.pathname : typeof event.properties?.path === "string" ? event.properties.path : undefined; return event.pathname ?? event.path ?? propertyPath ?? event.url ?? ""; } function eventName(event: RybbitEvent) { const propertyName = typeof event.properties?.event_name === "string" ? event.properties.event_name : typeof event.properties?.name === "string" ? event.properties.name : undefined; return event.event_name ?? event.name ?? propertyName ?? ""; } function eventDevice(event: RybbitEvent) { const value = event.properties?.deviceType ?? event.properties?.device_type ?? event.properties?.device; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function isAuditEvent(event: RybbitEvent, auditPath: string) { const path = eventPath(event); return path === auditPath || path.endsWith(auditPath); } export function summarizeAuditRybbitEvents( events: RybbitEvent[], auditPath: string, ): AuditRybbitSummary { const matchingEvents = events.filter((event) => isAuditEvent(event, auditPath)); const pageviews = matchingEvents.filter((event) => event.type === "pageview"); const ctaClicks = matchingEvents.filter((event) => { const name = eventName(event); return event.type === "custom_event" && name === "audit_cta_click"; }); const websiteLinkClicks = matchingEvents.filter((event) => { const name = eventName(event); return event.type === "outbound_link" || name === "audit_website_link_click"; }); const lastView = pageviews .map((event) => event.timestamp) .filter((timestamp): timestamp is string => Boolean(timestamp)) .sort() .at(-1) ?? null; const deviceTypes = [ ...new Set( matchingEvents .map(eventDevice) .filter((device): device is string => device !== null), ), ].sort(); return { opened: pageviews.length > 0, viewCount: pageviews.length, lastView, ctaClicks: ctaClicks.length, websiteLinkClicks: websiteLinkClicks.length, deviceTypes, }; } function normalizeEventsPayload(payload: unknown): RybbitEvent[] { if (Array.isArray(payload)) { return payload.filter((event): event is RybbitEvent => typeof event === "object" && event !== null); } if ( typeof payload === "object" && payload !== null && "data" in payload && Array.isArray((payload as { data?: unknown }).data) ) { return (payload as { data: unknown[] }).data.filter( (event): event is RybbitEvent => typeof event === "object" && event !== null, ); } return []; } export async function fetchRybbitAuditAnalytics(input: { apiUrl?: string; apiKey?: string; siteId?: string; auditPath: string; startDate?: string; endDate?: string; fetchImpl?: FetchLike; }) { if (!input.apiUrl || !input.apiKey || !input.siteId) { return { ok: false as const, error: "Rybbit ist nicht vollständig konfiguriert.", data: summarizeAuditRybbitEvents([], input.auditPath), }; } try { const response = await (input.fetchImpl ?? fetch)( buildRybbitEventsUrl({ apiUrl: input.apiUrl, siteId: input.siteId, startDate: input.startDate, endDate: input.endDate, }), { headers: { Authorization: `Bearer ${input.apiKey}`, }, }, ); if (!response.ok) { const body = await response.text(); return { ok: false as const, error: `Rybbit API Fehler ${response.status}: ${body.slice(0, 160)}`, data: summarizeAuditRybbitEvents([], input.auditPath), }; } const payload = await response.json(); return { ok: true as const, data: summarizeAuditRybbitEvents( normalizeEventsPayload(payload), input.auditPath, ), }; } catch (error) { return { ok: false as const, error: error instanceof Error ? error.message : String(error), data: summarizeAuditRybbitEvents([], input.auditPath), }; } }