metatron-open-seo/src/server/features/ga4/services/Ga4ReportDefinitions.ts

297 lines
7.7 KiB
TypeScript

import type { Ga4RunReportRequest } from "@/server/lib/ga4Client";
export type Ga4Channel = "organic_search" | "all";
export type Ga4ReportKind =
| "landing_pages"
| "page_performance"
| "key_events"
| "traffic_acquisition"
| "ecommerce_performance"
| "site_search"
| "audience_breakdown";
type Ga4ReportRequestInput = {
kind: Ga4ReportKind;
startDate: string;
endDate: string;
channel: Ga4Channel;
limit: number;
offset: number;
includeDate?: boolean;
breakdown?: "event" | "event_and_landing_page";
acquisitionBreakdown?: "channel_group" | "source_medium" | "campaign";
ecommerceBreakdown?: "item" | "landing_page";
ecommerceOnlyWithTransactions?: boolean;
audienceBreakdown?: "device" | "country" | "new_vs_returning";
};
const REPORT_DEFINITIONS = {
landing_pages: {
dimensions: ["hostName", "landingPage"],
metrics: [
"sessions",
"activeUsers",
"engagedSessions",
"engagementRate",
"keyEvents",
"sessionKeyEventRate",
"transactions",
"purchaseRevenue",
],
orderMetric: "sessions",
},
page_performance: {
dimensions: ["hostName", "pagePath"],
metrics: [
"screenPageViews",
"activeUsers",
"userEngagementDuration",
"keyEvents",
],
orderMetric: "screenPageViews",
},
key_events: {
dimensions: ["eventName"],
metrics: ["keyEvents", "totalUsers"],
orderMetric: "keyEvents",
},
traffic_acquisition: {
dimensions: ["sessionDefaultChannelGroup"],
metrics: [
"sessions",
"activeUsers",
"engagedSessions",
"engagementRate",
"keyEvents",
"transactions",
"purchaseRevenue",
],
orderMetric: "sessions",
},
ecommerce_performance: {
dimensions: ["itemName", "itemId"],
metrics: [
"itemsViewed",
"itemsAddedToCart",
"itemsPurchased",
"itemRevenue",
],
orderMetric: "itemRevenue",
},
site_search: {
dimensions: ["searchTerm"],
metrics: [
"eventCount",
"activeUsers",
"sessions",
"engagedSessions",
"engagementRate",
],
orderMetric: "eventCount",
},
audience_breakdown: {
dimensions: ["deviceCategory"],
metrics: ["activeUsers", "sessions", "engagementRate", "keyEvents"],
orderMetric: "activeUsers",
},
} as const;
export const OVERVIEW_METRICS = [
"sessions",
"activeUsers",
"engagedSessions",
"engagementRate",
"keyEvents",
"transactions",
"purchaseRevenue",
] as const;
function organicFilter() {
return {
filter: {
fieldName: "sessionDefaultChannelGroup",
stringFilter: { matchType: "EXACT", value: "Organic Search" },
},
};
}
function reportDefinition(input: Ga4ReportRequestInput) {
if (
input.kind === "ecommerce_performance" &&
input.ecommerceBreakdown === "landing_page"
) {
return {
dimensions: ["hostName", "landingPage"] as const,
metrics: ["sessions", "transactions", "purchaseRevenue"] as const,
orderMetric: "purchaseRevenue",
};
}
return REPORT_DEFINITIONS[input.kind];
}
function reportDimensions(
input: Ga4ReportRequestInput,
defaults: readonly string[],
): string[] {
if (input.kind === "traffic_acquisition") {
return [
{
channel_group: "sessionDefaultChannelGroup",
source_medium: "sessionSourceMedium",
campaign: "sessionCampaignName",
}[input.acquisitionBreakdown ?? "channel_group"],
];
}
if (input.kind === "audience_breakdown") {
return [
{
device: "deviceCategory",
country: "country",
new_vs_returning: "newVsReturning",
}[input.audienceBreakdown ?? "device"],
];
}
const dimensions = [...defaults];
if (input.kind === "page_performance" && input.includeDate) {
dimensions.push("date");
}
if (
input.kind === "key_events" &&
input.breakdown === "event_and_landing_page"
) {
dimensions.push("hostName", "landingPage");
}
return dimensions;
}
function reportFilter(input: Ga4ReportRequestInput): unknown {
if (input.kind === "site_search") {
return {
andGroup: {
expressions: [
{
filter: {
fieldName: "eventName",
stringFilter: {
matchType: "EXACT",
value: "view_search_results",
},
},
},
{
notExpression: {
filter: {
fieldName: "searchTerm",
stringFilter: { matchType: "EXACT", value: "(not set)" },
},
},
},
],
},
};
}
return input.channel === "organic_search" ? organicFilter() : undefined;
}
function metricFilter(input: Ga4ReportRequestInput): unknown {
if (input.kind === "key_events") {
return {
filter: {
fieldName: "keyEvents",
numericFilter: {
operation: "GREATER_THAN",
value: { doubleValue: 0 },
},
},
};
}
if (
input.kind === "ecommerce_performance" &&
input.ecommerceBreakdown === "landing_page" &&
input.ecommerceOnlyWithTransactions
) {
return {
filter: {
fieldName: "transactions",
numericFilter: {
operation: "GREATER_THAN",
value: { doubleValue: 0 },
},
},
};
}
return undefined;
}
function effectiveBreakdown(input: Ga4ReportRequestInput): string {
if (input.kind === "landing_pages") return "landing_page";
if (input.kind === "page_performance") {
return input.includeDate ? "page_and_date" : "page";
}
if (input.kind === "key_events") return input.breakdown ?? "event";
if (input.kind === "traffic_acquisition") {
return input.acquisitionBreakdown ?? "channel_group";
}
if (input.kind === "ecommerce_performance") {
return input.ecommerceBreakdown ?? "item";
}
if (input.kind === "site_search") return "search_term";
return input.audienceBreakdown ?? "device";
}
export function getGa4ReportConfiguration(input: Ga4ReportRequestInput) {
const definition = reportDefinition(input);
return {
reportKind: input.kind,
breakdown: effectiveBreakdown(input),
dimensions: reportDimensions(input, definition.dimensions),
metrics: [...definition.metrics],
flags: {
includeDate: input.includeDate ?? false,
onlyWithTransactions: input.ecommerceOnlyWithTransactions ?? false,
},
};
}
export function buildGa4ReportRequest(
input: Ga4ReportRequestInput,
): Ga4RunReportRequest {
const definition = reportDefinition(input);
const dimensions = reportDimensions(input, definition.dimensions);
return {
dateRanges: [{ startDate: input.startDate, endDate: input.endDate }],
dimensions: dimensions.map((name) => ({ name })),
metrics: definition.metrics.map((name) => ({ name })),
dimensionFilter: reportFilter(input),
metricFilter: metricFilter(input),
offset: String(input.offset),
limit: String(input.limit),
orderBys: [{ metric: { metricName: definition.orderMetric }, desc: true }],
keepEmptyRows: false,
returnPropertyQuota: true,
};
}
export function buildGa4OverviewRequest(input: {
startDate: string;
endDate: string;
trend?: "daily" | "weekly";
}): Ga4RunReportRequest {
const dimensions = input.trend
? [{ name: input.trend === "daily" ? "date" : "yearWeek" }]
: [];
return {
dateRanges: [{ startDate: input.startDate, endDate: input.endDate }],
dimensions,
metrics: OVERVIEW_METRICS.map((name) => ({ name })),
dimensionFilter: organicFilter(),
offset: "0",
limit: input.trend ? "1000" : "1",
orderBys: input.trend
? [{ dimension: { dimensionName: dimensions[0]?.name ?? "date" } }]
: [],
keepEmptyRows: false,
returnPropertyQuota: true,
};
}