299 lines
7.8 KiB
TypeScript
299 lines
7.8 KiB
TypeScript
export type RybbitEvent = {
|
|
type?: string;
|
|
timestamp?: string;
|
|
pathname?: string;
|
|
path?: string;
|
|
url?: string;
|
|
event_name?: string;
|
|
name?: string;
|
|
properties?: Record<string, unknown>;
|
|
};
|
|
|
|
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<string, {
|
|
auditOpens: number;
|
|
ctaClicks: number;
|
|
outboundClicks: number;
|
|
}>;
|
|
};
|
|
|
|
type FetchLike = (
|
|
input: string | URL,
|
|
init?: RequestInit,
|
|
) => Promise<Pick<Response, "ok" | "status" | "json" | "text">>;
|
|
|
|
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([]),
|
|
};
|
|
}
|
|
}
|