Add audit analytics and campaign metrics
This commit is contained in:
72
tests/analytics-source.test.ts
Normal file
72
tests/analytics-source.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
function source(path: string) {
|
||||
return readFileSync(join(process.cwd(), ...path.split("/")), "utf8");
|
||||
}
|
||||
|
||||
test("Rybbit tracking is mounted only in public audit presentation", () => {
|
||||
const publicAuditSource = source("components/public-audit/public-audit-page.tsx");
|
||||
const dashboardLayoutSource = source("app/dashboard/layout.tsx");
|
||||
const dashboardAnalyticsSource = source("app/dashboard/analytics/page.tsx");
|
||||
|
||||
assert.match(publicAuditSource, /RybbitTracking/);
|
||||
assert.match(publicAuditSource, /TrackedPublicAuditLink/);
|
||||
assert.doesNotMatch(dashboardLayoutSource, /RybbitTracking|rybbit/i);
|
||||
assert.doesNotMatch(dashboardAnalyticsSource, /next\/script|RybbitTracking/);
|
||||
});
|
||||
|
||||
test("internal Rybbit route fetches audit analytics on demand with graceful errors", () => {
|
||||
const routePath = "app/api/internal/rybbit/audit/route.ts";
|
||||
assert.equal(existsSync(join(process.cwd(), ...routePath.split("/"))), true);
|
||||
const routeSource = source(routePath);
|
||||
|
||||
assert.match(routeSource, /export async function GET/);
|
||||
assert.match(routeSource, /fetchRybbitAuditAnalytics/);
|
||||
assert.match(routeSource, /RYBBIT_API_KEY/);
|
||||
assert.match(routeSource, /return Response\.json\(\{ ok: false/);
|
||||
});
|
||||
|
||||
test("campaign metrics query exposes lightweight funnel and run metrics", () => {
|
||||
const metricsSource = source("convex/campaignMetrics.ts");
|
||||
|
||||
assert.match(metricsSource, /export const getDashboard = query/);
|
||||
for (const label of [
|
||||
"foundLeads",
|
||||
"leadsWithContact",
|
||||
"missingContact",
|
||||
"auditsCreated",
|
||||
"approvalsOpen",
|
||||
"emailsSent",
|
||||
"followUpsPlanned",
|
||||
"followUpsSent",
|
||||
"responses",
|
||||
"conversations",
|
||||
"offers",
|
||||
"wins",
|
||||
"losses",
|
||||
"skippedDuplicates",
|
||||
"skippedBlacklisted",
|
||||
]) {
|
||||
assert.match(metricsSource, new RegExp(label));
|
||||
}
|
||||
});
|
||||
|
||||
test("analytics dashboard renders filters, Convex metrics, and Rybbit error states", () => {
|
||||
const pageSource = source("app/dashboard/analytics/page.tsx");
|
||||
const componentSource = source("components/analytics/analytics-dashboard.tsx");
|
||||
|
||||
assert.doesNotMatch(pageSource, /DashboardPlaceholderPage/);
|
||||
assert.match(pageSource, /AnalyticsDashboard/);
|
||||
assert.match(componentSource, /api\.campaignMetrics\.getDashboard/);
|
||||
assert.match(componentSource, /Kampagne/);
|
||||
assert.match(componentSource, /Nische/);
|
||||
assert.match(componentSource, /PLZ/);
|
||||
assert.match(componentSource, /Radius/);
|
||||
assert.match(componentSource, /Priorität/);
|
||||
assert.match(componentSource, /Status/);
|
||||
assert.match(componentSource, /Zeitraum/);
|
||||
assert.match(componentSource, /Rybbit-Daten konnten nicht geladen werden/);
|
||||
});
|
||||
72
tests/rybbit-analytics.test.ts
Normal file
72
tests/rybbit-analytics.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildRybbitEventsUrl,
|
||||
summarizeAuditRybbitEvents,
|
||||
type RybbitEvent,
|
||||
} from "../lib/rybbit-analytics";
|
||||
|
||||
test("buildRybbitEventsUrl targets the documented events endpoint", () => {
|
||||
const url = buildRybbitEventsUrl({
|
||||
apiUrl: "https://analytics.example.com/",
|
||||
siteId: "site_123",
|
||||
startDate: "2026-06-01T00:00:00.000Z",
|
||||
endDate: "2026-06-05T00:00:00.000Z",
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
url.toString(),
|
||||
"https://analytics.example.com/api/sites/site_123/events?start_date=2026-06-01T00%3A00%3A00.000Z&end_date=2026-06-05T00%3A00%3A00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
test("summarizeAuditRybbitEvents extracts opens, clicks, last view, and devices", () => {
|
||||
const events: RybbitEvent[] = [
|
||||
{
|
||||
type: "pageview",
|
||||
timestamp: "2026-06-05T10:00:00.000Z",
|
||||
pathname: "/audit/demo",
|
||||
properties: { device: "desktop" },
|
||||
},
|
||||
{
|
||||
type: "custom_event",
|
||||
timestamp: "2026-06-05T10:05:00.000Z",
|
||||
event_name: "audit_cta_click",
|
||||
pathname: "/audit/demo",
|
||||
properties: { target: "cta", deviceType: "mobile" },
|
||||
},
|
||||
{
|
||||
type: "outbound_link",
|
||||
timestamp: "2026-06-05T10:06:00.000Z",
|
||||
pathname: "/audit/demo",
|
||||
properties: { href: "https://example.com", device: "mobile" },
|
||||
},
|
||||
{
|
||||
type: "pageview",
|
||||
timestamp: "2026-06-05T11:00:00.000Z",
|
||||
pathname: "/pricing",
|
||||
properties: { device: "desktop" },
|
||||
},
|
||||
];
|
||||
|
||||
assert.deepEqual(summarizeAuditRybbitEvents(events, "/audit/demo"), {
|
||||
opened: true,
|
||||
viewCount: 1,
|
||||
lastView: "2026-06-05T10:00:00.000Z",
|
||||
ctaClicks: 1,
|
||||
websiteLinkClicks: 1,
|
||||
deviceTypes: ["desktop", "mobile"],
|
||||
});
|
||||
});
|
||||
|
||||
test("summarizeAuditRybbitEvents returns graceful empty metrics", () => {
|
||||
assert.deepEqual(summarizeAuditRybbitEvents([], "/audit/demo"), {
|
||||
opened: false,
|
||||
viewCount: 0,
|
||||
lastView: null,
|
||||
ctaClicks: 0,
|
||||
websiteLinkClicks: 0,
|
||||
deviceTypes: [],
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user