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[]; }; export type CampaignRybbitSummary = { auditOpens: number; ctaClicks: number; outboundClicks: number; byPath: Record; }; 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, }; } export function summarizeCampaignRybbitEvents( events: RybbitEvent[], ): CampaignRybbitSummary { const auditEvents = events.filter((event) => eventPath(event).startsWith("/audit/")); const byPath: CampaignRybbitSummary["byPath"] = {}; for (const event of auditEvents) { const path = eventPath(event); byPath[path] ??= { auditOpens: 0, ctaClicks: 0, outboundClicks: 0 }; if (event.type === "pageview") { byPath[path].auditOpens += 1; } if (event.type === "custom_event" && eventName(event) === "audit_cta_click") { byPath[path].ctaClicks += 1; } if ( event.type === "outbound_link" || eventName(event) === "audit_website_link_click" ) { byPath[path].outboundClicks += 1; } } return { auditOpens: auditEvents.filter((event) => event.type === "pageview").length, ctaClicks: auditEvents.filter((event) => { return event.type === "custom_event" && eventName(event) === "audit_cta_click"; }).length, outboundClicks: auditEvents.filter((event) => { return event.type === "outbound_link" || eventName(event) === "audit_website_link_click"; }).length, byPath, }; } 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), }; } } export async function fetchRybbitCampaignAnalytics(input: { apiUrl?: string; apiKey?: string; siteId?: 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: summarizeCampaignRybbitEvents([]), }; } 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: summarizeCampaignRybbitEvents([]), }; } return { ok: true as const, data: summarizeCampaignRybbitEvents( normalizeEventsPayload(await response.json()), ), }; } catch (error) { return { ok: false as const, error: error instanceof Error ? error.message : String(error), data: summarizeCampaignRybbitEvents([]), }; } }