297 lines
7.7 KiB
TypeScript
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,
|
|
};
|
|
}
|