sanity-plugin-ga-dashboard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.mts +15 -0
- package/dist/api/index.d.ts +15 -0
- package/dist/api/index.esm.js +235 -0
- package/dist/api/index.esm.js.map +1 -0
- package/dist/api/index.js +260 -0
- package/dist/api/index.js.map +1 -0
- package/dist/index.d.mts +117 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.esm.js +880 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +890 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router compatible GET handler.
|
|
3
|
+
*
|
|
4
|
+
* Usage in apps/web/src/app/api/analytics/route.ts:
|
|
5
|
+
*
|
|
6
|
+
* export { GET } from 'sanity-plugin-ga-dashboard/api'
|
|
7
|
+
*
|
|
8
|
+
* Required environment variables:
|
|
9
|
+
* GA_PROPERTY_ID - Numeric GA4 property ID
|
|
10
|
+
* GA_SERVICE_ACCOUNT_EMAIL - Service account client_email
|
|
11
|
+
* GA_PRIVATE_KEY - Service account private_key
|
|
12
|
+
*/
|
|
13
|
+
declare function GET(request: Request): Promise<Response>;
|
|
14
|
+
|
|
15
|
+
export { GET };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router compatible GET handler.
|
|
3
|
+
*
|
|
4
|
+
* Usage in apps/web/src/app/api/analytics/route.ts:
|
|
5
|
+
*
|
|
6
|
+
* export { GET } from 'sanity-plugin-ga-dashboard/api'
|
|
7
|
+
*
|
|
8
|
+
* Required environment variables:
|
|
9
|
+
* GA_PROPERTY_ID - Numeric GA4 property ID
|
|
10
|
+
* GA_SERVICE_ACCOUNT_EMAIL - Service account client_email
|
|
11
|
+
* GA_PRIVATE_KEY - Service account private_key
|
|
12
|
+
*/
|
|
13
|
+
declare function GET(request: Request): Promise<Response>;
|
|
14
|
+
|
|
15
|
+
export { GET };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// src/api/index.ts
|
|
2
|
+
import { SignJWT, importPKCS8 } from "jose";
|
|
3
|
+
var GA_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
4
|
+
var GA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
|
|
5
|
+
var GA_SCOPE = "https://www.googleapis.com/auth/analytics.readonly";
|
|
6
|
+
var cachedToken = null;
|
|
7
|
+
async function getAccessToken() {
|
|
8
|
+
if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value;
|
|
9
|
+
const clientEmail = process.env.GA_SERVICE_ACCOUNT_EMAIL;
|
|
10
|
+
const privateKeyRaw = process.env.GA_PRIVATE_KEY;
|
|
11
|
+
if (!clientEmail || !privateKeyRaw)
|
|
12
|
+
throw new Error("Missing GA_SERVICE_ACCOUNT_EMAIL or GA_PRIVATE_KEY env vars");
|
|
13
|
+
const privateKey = privateKeyRaw.replace(/\\n/g, "\n");
|
|
14
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
15
|
+
const key = await importPKCS8(privateKey, "RS256");
|
|
16
|
+
const jwt = await new SignJWT({ scope: GA_SCOPE }).setProtectedHeader({ alg: "RS256" }).setIssuedAt(now).setExpirationTime(now + 3600).setIssuer(clientEmail).setAudience(GA_TOKEN_URL).sign(key);
|
|
17
|
+
const tokenRes = await fetch(GA_TOKEN_URL, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
20
|
+
body: new URLSearchParams({
|
|
21
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
22
|
+
assertion: jwt
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
if (!tokenRes.ok) throw new Error(`Failed to obtain access token: ${await tokenRes.text()}`);
|
|
26
|
+
const { access_token, expires_in } = await tokenRes.json();
|
|
27
|
+
cachedToken = { value: access_token, expiresAt: Date.now() + (expires_in - 60) * 1e3 };
|
|
28
|
+
return access_token;
|
|
29
|
+
}
|
|
30
|
+
async function report(propertyId, token, body) {
|
|
31
|
+
var _a;
|
|
32
|
+
const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runReport`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify(body)
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const err = await res.json().catch(() => null);
|
|
39
|
+
throw new Error(
|
|
40
|
+
((_a = err == null ? void 0 : err.error) == null ? void 0 : _a.message) || `GA API error ${res.status}: ${res.statusText}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return res.json();
|
|
44
|
+
}
|
|
45
|
+
async function realtimeReport(propertyId, token, body) {
|
|
46
|
+
const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runRealtimeReport`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(body)
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) return {};
|
|
52
|
+
return res.json();
|
|
53
|
+
}
|
|
54
|
+
async function GET(request) {
|
|
55
|
+
try {
|
|
56
|
+
const propertyId = process.env.GA_PROPERTY_ID;
|
|
57
|
+
if (!propertyId)
|
|
58
|
+
return Response.json({ error: "GA_PROPERTY_ID not configured" }, { status: 500 });
|
|
59
|
+
const { searchParams } = new URL(request.url);
|
|
60
|
+
const dateRange = searchParams.get("range") || "30";
|
|
61
|
+
const startDate = `${dateRange}daysAgo`;
|
|
62
|
+
let token;
|
|
63
|
+
try {
|
|
64
|
+
token = await getAccessToken();
|
|
65
|
+
} catch (authErr) {
|
|
66
|
+
const msg = authErr instanceof Error ? authErr.message : String(authErr);
|
|
67
|
+
console.error("[analytics] Auth error:", msg);
|
|
68
|
+
return Response.json({ error: `Auth failed: ${msg}` }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
const settled = await Promise.allSettled([
|
|
71
|
+
// 0. Overview metrics
|
|
72
|
+
report(propertyId, token, {
|
|
73
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
74
|
+
metrics: [
|
|
75
|
+
{ name: "totalUsers" },
|
|
76
|
+
{ name: "newUsers" },
|
|
77
|
+
{ name: "sessions" },
|
|
78
|
+
{ name: "screenPageViews" },
|
|
79
|
+
{ name: "averageSessionDuration" },
|
|
80
|
+
{ name: "bounceRate" },
|
|
81
|
+
{ name: "engagedSessions" },
|
|
82
|
+
{ name: "engagementRate" },
|
|
83
|
+
{ name: "screenPageViewsPerSession" },
|
|
84
|
+
{ name: "eventsPerSession" }
|
|
85
|
+
]
|
|
86
|
+
}),
|
|
87
|
+
// 1. Time series by date
|
|
88
|
+
report(propertyId, token, {
|
|
89
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
90
|
+
dimensions: [{ name: "date" }],
|
|
91
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }, { name: "screenPageViews" }],
|
|
92
|
+
orderBys: [{ dimension: { dimensionName: "date" }, desc: false }]
|
|
93
|
+
}),
|
|
94
|
+
// 2. Hourly traffic (today only)
|
|
95
|
+
report(propertyId, token, {
|
|
96
|
+
dateRanges: [{ startDate: "today", endDate: "today" }],
|
|
97
|
+
dimensions: [{ name: "hour" }],
|
|
98
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }],
|
|
99
|
+
orderBys: [{ dimension: { dimensionName: "hour" }, desc: false }]
|
|
100
|
+
}),
|
|
101
|
+
// 3. Top pages
|
|
102
|
+
report(propertyId, token, {
|
|
103
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
104
|
+
dimensions: [{ name: "pagePath" }],
|
|
105
|
+
metrics: [{ name: "screenPageViews" }, { name: "totalUsers" }, { name: "averageSessionDuration" }],
|
|
106
|
+
orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
|
|
107
|
+
limit: 15
|
|
108
|
+
}),
|
|
109
|
+
// 4. Top landing pages
|
|
110
|
+
report(propertyId, token, {
|
|
111
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
112
|
+
dimensions: [{ name: "landingPage" }],
|
|
113
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }, { name: "bounceRate" }],
|
|
114
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
115
|
+
limit: 10
|
|
116
|
+
}),
|
|
117
|
+
// 5. Device category
|
|
118
|
+
report(propertyId, token, {
|
|
119
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
120
|
+
dimensions: [{ name: "deviceCategory" }],
|
|
121
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }]
|
|
122
|
+
}),
|
|
123
|
+
// 6. Browser breakdown
|
|
124
|
+
report(propertyId, token, {
|
|
125
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
126
|
+
dimensions: [{ name: "browser" }],
|
|
127
|
+
metrics: [{ name: "sessions" }],
|
|
128
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
129
|
+
limit: 8
|
|
130
|
+
}),
|
|
131
|
+
// 7. Operating system
|
|
132
|
+
report(propertyId, token, {
|
|
133
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
134
|
+
dimensions: [{ name: "operatingSystem" }],
|
|
135
|
+
metrics: [{ name: "sessions" }],
|
|
136
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
137
|
+
limit: 8
|
|
138
|
+
}),
|
|
139
|
+
// 8. Top countries
|
|
140
|
+
report(propertyId, token, {
|
|
141
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
142
|
+
dimensions: [{ name: "country" }],
|
|
143
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }],
|
|
144
|
+
orderBys: [{ metric: { metricName: "totalUsers" }, desc: true }],
|
|
145
|
+
limit: 10
|
|
146
|
+
}),
|
|
147
|
+
// 9. Top cities
|
|
148
|
+
report(propertyId, token, {
|
|
149
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
150
|
+
dimensions: [{ name: "city" }, { name: "country" }],
|
|
151
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }],
|
|
152
|
+
orderBys: [{ metric: { metricName: "totalUsers" }, desc: true }],
|
|
153
|
+
limit: 10
|
|
154
|
+
}),
|
|
155
|
+
// 10. Traffic sources
|
|
156
|
+
report(propertyId, token, {
|
|
157
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
158
|
+
dimensions: [{ name: "sessionSource" }, { name: "sessionMedium" }],
|
|
159
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
160
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
161
|
+
limit: 10
|
|
162
|
+
}),
|
|
163
|
+
// 11. Channel grouping
|
|
164
|
+
report(propertyId, token, {
|
|
165
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
166
|
+
dimensions: [{ name: "sessionDefaultChannelGroup" }],
|
|
167
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }, { name: "engagementRate" }],
|
|
168
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }]
|
|
169
|
+
}),
|
|
170
|
+
// 12. New vs returning users
|
|
171
|
+
report(propertyId, token, {
|
|
172
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
173
|
+
dimensions: [{ name: "newVsReturning" }],
|
|
174
|
+
metrics: [{ name: "totalUsers" }]
|
|
175
|
+
}),
|
|
176
|
+
// 13. Top events
|
|
177
|
+
report(propertyId, token, {
|
|
178
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
179
|
+
dimensions: [{ name: "eventName" }],
|
|
180
|
+
metrics: [{ name: "eventCount" }, { name: "totalUsers" }],
|
|
181
|
+
orderBys: [{ metric: { metricName: "eventCount" }, desc: true }],
|
|
182
|
+
limit: 15
|
|
183
|
+
}),
|
|
184
|
+
// 14. Top referrers
|
|
185
|
+
report(propertyId, token, {
|
|
186
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
187
|
+
dimensions: [{ name: "pageReferrer" }],
|
|
188
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
189
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
190
|
+
limit: 10
|
|
191
|
+
}),
|
|
192
|
+
// 15. Real-time active users (last 30 min)
|
|
193
|
+
realtimeReport(propertyId, token, {
|
|
194
|
+
metrics: [{ name: "activeUsers" }]
|
|
195
|
+
})
|
|
196
|
+
]);
|
|
197
|
+
settled.forEach((s, i) => {
|
|
198
|
+
if (s.status === "rejected")
|
|
199
|
+
console.error(`[analytics] report[${i}] failed:`, s.reason);
|
|
200
|
+
});
|
|
201
|
+
const ok = (i) => {
|
|
202
|
+
const s = settled[i];
|
|
203
|
+
return s && s.status === "fulfilled" ? s.value : {};
|
|
204
|
+
};
|
|
205
|
+
return Response.json(
|
|
206
|
+
{
|
|
207
|
+
activeUsers: ok(15),
|
|
208
|
+
overview: ok(0),
|
|
209
|
+
timeSeries: ok(1),
|
|
210
|
+
hourlyToday: ok(2),
|
|
211
|
+
topPages: ok(3),
|
|
212
|
+
landingPages: ok(4),
|
|
213
|
+
devices: ok(5),
|
|
214
|
+
browsers: ok(6),
|
|
215
|
+
operatingSystems: ok(7),
|
|
216
|
+
countries: ok(8),
|
|
217
|
+
cities: ok(9),
|
|
218
|
+
trafficSources: ok(10),
|
|
219
|
+
channels: ok(11),
|
|
220
|
+
newVsReturning: ok(12),
|
|
221
|
+
topEvents: ok(13),
|
|
222
|
+
referrers: ok(14)
|
|
223
|
+
},
|
|
224
|
+
{ headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60" } }
|
|
225
|
+
);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
228
|
+
console.error("[analytics] Unhandled error:", message);
|
|
229
|
+
return Response.json({ error: message }, { status: 500 });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export {
|
|
233
|
+
GET
|
|
234
|
+
};
|
|
235
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/index.ts"],"sourcesContent":["import {SignJWT, importPKCS8} from 'jose'\n\nconst GA_TOKEN_URL = 'https://oauth2.googleapis.com/token'\nconst GA_API_BASE = 'https://analyticsdata.googleapis.com/v1beta'\nconst GA_SCOPE = 'https://www.googleapis.com/auth/analytics.readonly'\n\nlet cachedToken: {value: string; expiresAt: number} | null = null\n\nasync function getAccessToken(): Promise<string> {\n if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value\n\n const clientEmail = process.env.GA_SERVICE_ACCOUNT_EMAIL\n const privateKeyRaw = process.env.GA_PRIVATE_KEY\n if (!clientEmail || !privateKeyRaw)\n throw new Error('Missing GA_SERVICE_ACCOUNT_EMAIL or GA_PRIVATE_KEY env vars')\n\n const privateKey = privateKeyRaw.replace(/\\\\n/g, '\\n')\n const now = Math.floor(Date.now() / 1000)\n const key = await importPKCS8(privateKey, 'RS256')\n\n const jwt = await new SignJWT({scope: GA_SCOPE})\n .setProtectedHeader({alg: 'RS256'})\n .setIssuedAt(now)\n .setExpirationTime(now + 3600)\n .setIssuer(clientEmail)\n .setAudience(GA_TOKEN_URL)\n .sign(key)\n\n const tokenRes = await fetch(GA_TOKEN_URL, {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }),\n })\n if (!tokenRes.ok) throw new Error(`Failed to obtain access token: ${await tokenRes.text()}`)\n\n const {access_token, expires_in} = await tokenRes.json()\n cachedToken = {value: access_token, expiresAt: Date.now() + (expires_in - 60) * 1000}\n return access_token\n}\n\nasync function report(\n propertyId: string,\n token: string,\n body: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runReport`, {\n method: 'POST',\n headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => null)\n throw new Error(\n (err as {error?: {message?: string}})?.error?.message ||\n `GA API error ${res.status}: ${res.statusText}`,\n )\n }\n return res.json()\n}\n\nasync function realtimeReport(\n propertyId: string,\n token: string,\n body: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runRealtimeReport`, {\n method: 'POST',\n headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},\n body: JSON.stringify(body),\n })\n if (!res.ok) return {}\n return res.json()\n}\n\n/**\n * Next.js App Router compatible GET handler.\n *\n * Usage in apps/web/src/app/api/analytics/route.ts:\n *\n * export { GET } from 'sanity-plugin-ga-dashboard/api'\n *\n * Required environment variables:\n * GA_PROPERTY_ID - Numeric GA4 property ID\n * GA_SERVICE_ACCOUNT_EMAIL - Service account client_email\n * GA_PRIVATE_KEY - Service account private_key\n */\nexport async function GET(request: Request): Promise<Response> {\n try {\n const propertyId = process.env.GA_PROPERTY_ID\n if (!propertyId)\n return Response.json({error: 'GA_PROPERTY_ID not configured'}, {status: 500})\n\n const {searchParams} = new URL(request.url)\n const dateRange = searchParams.get('range') || '30'\n const startDate = `${dateRange}daysAgo`\n\n let token: string\n try {\n token = await getAccessToken()\n } catch (authErr: unknown) {\n const msg = authErr instanceof Error ? authErr.message : String(authErr)\n console.error('[analytics] Auth error:', msg)\n return Response.json({error: `Auth failed: ${msg}`}, {status: 500})\n }\n\n const settled = await Promise.allSettled([\n // 0. Overview metrics\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n metrics: [\n {name: 'totalUsers'},\n {name: 'newUsers'},\n {name: 'sessions'},\n {name: 'screenPageViews'},\n {name: 'averageSessionDuration'},\n {name: 'bounceRate'},\n {name: 'engagedSessions'},\n {name: 'engagementRate'},\n {name: 'screenPageViewsPerSession'},\n {name: 'eventsPerSession'},\n ],\n }),\n // 1. Time series by date\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'date'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}, {name: 'screenPageViews'}],\n orderBys: [{dimension: {dimensionName: 'date'}, desc: false}],\n }),\n // 2. Hourly traffic (today only)\n report(propertyId, token, {\n dateRanges: [{startDate: 'today', endDate: 'today'}],\n dimensions: [{name: 'hour'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}],\n orderBys: [{dimension: {dimensionName: 'hour'}, desc: false}],\n }),\n // 3. Top pages\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'pagePath'}],\n metrics: [{name: 'screenPageViews'}, {name: 'totalUsers'}, {name: 'averageSessionDuration'}],\n orderBys: [{metric: {metricName: 'screenPageViews'}, desc: true}],\n limit: 15,\n }),\n // 4. Top landing pages\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'landingPage'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}, {name: 'bounceRate'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 10,\n }),\n // 5. Device category\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'deviceCategory'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}],\n }),\n // 6. Browser breakdown\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'browser'}],\n metrics: [{name: 'sessions'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 8,\n }),\n // 7. Operating system\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'operatingSystem'}],\n metrics: [{name: 'sessions'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 8,\n }),\n // 8. Top countries\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'country'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}],\n orderBys: [{metric: {metricName: 'totalUsers'}, desc: true}],\n limit: 10,\n }),\n // 9. Top cities\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'city'}, {name: 'country'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}],\n orderBys: [{metric: {metricName: 'totalUsers'}, desc: true}],\n limit: 10,\n }),\n // 10. Traffic sources\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'sessionSource'}, {name: 'sessionMedium'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 10,\n }),\n // 11. Channel grouping\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'sessionDefaultChannelGroup'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}, {name: 'engagementRate'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n }),\n // 12. New vs returning users\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'newVsReturning'}],\n metrics: [{name: 'totalUsers'}],\n }),\n // 13. Top events\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'eventName'}],\n metrics: [{name: 'eventCount'}, {name: 'totalUsers'}],\n orderBys: [{metric: {metricName: 'eventCount'}, desc: true}],\n limit: 15,\n }),\n // 14. Top referrers\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'pageReferrer'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 10,\n }),\n // 15. Real-time active users (last 30 min)\n realtimeReport(propertyId, token, {\n metrics: [{name: 'activeUsers'}],\n }),\n ])\n\n settled.forEach((s, i) => {\n if (s.status === 'rejected')\n console.error(`[analytics] report[${i}] failed:`, s.reason)\n })\n\n const ok = (i: number) => {\n const s = settled[i]\n return s && s.status === 'fulfilled' ? (s as PromiseFulfilledResult<Record<string, unknown>>).value : {}\n }\n\n return Response.json(\n {\n activeUsers: ok(15),\n overview: ok(0),\n timeSeries: ok(1),\n hourlyToday: ok(2),\n topPages: ok(3),\n landingPages: ok(4),\n devices: ok(5),\n browsers: ok(6),\n operatingSystems: ok(7),\n countries: ok(8),\n cities: ok(9),\n trafficSources: ok(10),\n channels: ok(11),\n newVsReturning: ok(12),\n topEvents: ok(13),\n referrers: ok(14),\n },\n {headers: {'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60'}},\n )\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n console.error('[analytics] Unhandled error:', message)\n return Response.json({error: message}, {status: 500})\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/api/index.ts
|
|
21
|
+
var api_exports = {};
|
|
22
|
+
__export(api_exports, {
|
|
23
|
+
GET: () => GET
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(api_exports);
|
|
26
|
+
var import_jose = require("jose");
|
|
27
|
+
var GA_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
28
|
+
var GA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
|
|
29
|
+
var GA_SCOPE = "https://www.googleapis.com/auth/analytics.readonly";
|
|
30
|
+
var cachedToken = null;
|
|
31
|
+
async function getAccessToken() {
|
|
32
|
+
if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value;
|
|
33
|
+
const clientEmail = process.env.GA_SERVICE_ACCOUNT_EMAIL;
|
|
34
|
+
const privateKeyRaw = process.env.GA_PRIVATE_KEY;
|
|
35
|
+
if (!clientEmail || !privateKeyRaw)
|
|
36
|
+
throw new Error("Missing GA_SERVICE_ACCOUNT_EMAIL or GA_PRIVATE_KEY env vars");
|
|
37
|
+
const privateKey = privateKeyRaw.replace(/\\n/g, "\n");
|
|
38
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
39
|
+
const key = await (0, import_jose.importPKCS8)(privateKey, "RS256");
|
|
40
|
+
const jwt = await new import_jose.SignJWT({ scope: GA_SCOPE }).setProtectedHeader({ alg: "RS256" }).setIssuedAt(now).setExpirationTime(now + 3600).setIssuer(clientEmail).setAudience(GA_TOKEN_URL).sign(key);
|
|
41
|
+
const tokenRes = await fetch(GA_TOKEN_URL, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
44
|
+
body: new URLSearchParams({
|
|
45
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
46
|
+
assertion: jwt
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
if (!tokenRes.ok) throw new Error(`Failed to obtain access token: ${await tokenRes.text()}`);
|
|
50
|
+
const { access_token, expires_in } = await tokenRes.json();
|
|
51
|
+
cachedToken = { value: access_token, expiresAt: Date.now() + (expires_in - 60) * 1e3 };
|
|
52
|
+
return access_token;
|
|
53
|
+
}
|
|
54
|
+
async function report(propertyId, token, body) {
|
|
55
|
+
var _a;
|
|
56
|
+
const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runReport`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify(body)
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const err = await res.json().catch(() => null);
|
|
63
|
+
throw new Error(
|
|
64
|
+
((_a = err == null ? void 0 : err.error) == null ? void 0 : _a.message) || `GA API error ${res.status}: ${res.statusText}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return res.json();
|
|
68
|
+
}
|
|
69
|
+
async function realtimeReport(propertyId, token, body) {
|
|
70
|
+
const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runRealtimeReport`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify(body)
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) return {};
|
|
76
|
+
return res.json();
|
|
77
|
+
}
|
|
78
|
+
async function GET(request) {
|
|
79
|
+
try {
|
|
80
|
+
const propertyId = process.env.GA_PROPERTY_ID;
|
|
81
|
+
if (!propertyId)
|
|
82
|
+
return Response.json({ error: "GA_PROPERTY_ID not configured" }, { status: 500 });
|
|
83
|
+
const { searchParams } = new URL(request.url);
|
|
84
|
+
const dateRange = searchParams.get("range") || "30";
|
|
85
|
+
const startDate = `${dateRange}daysAgo`;
|
|
86
|
+
let token;
|
|
87
|
+
try {
|
|
88
|
+
token = await getAccessToken();
|
|
89
|
+
} catch (authErr) {
|
|
90
|
+
const msg = authErr instanceof Error ? authErr.message : String(authErr);
|
|
91
|
+
console.error("[analytics] Auth error:", msg);
|
|
92
|
+
return Response.json({ error: `Auth failed: ${msg}` }, { status: 500 });
|
|
93
|
+
}
|
|
94
|
+
const settled = await Promise.allSettled([
|
|
95
|
+
// 0. Overview metrics
|
|
96
|
+
report(propertyId, token, {
|
|
97
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
98
|
+
metrics: [
|
|
99
|
+
{ name: "totalUsers" },
|
|
100
|
+
{ name: "newUsers" },
|
|
101
|
+
{ name: "sessions" },
|
|
102
|
+
{ name: "screenPageViews" },
|
|
103
|
+
{ name: "averageSessionDuration" },
|
|
104
|
+
{ name: "bounceRate" },
|
|
105
|
+
{ name: "engagedSessions" },
|
|
106
|
+
{ name: "engagementRate" },
|
|
107
|
+
{ name: "screenPageViewsPerSession" },
|
|
108
|
+
{ name: "eventsPerSession" }
|
|
109
|
+
]
|
|
110
|
+
}),
|
|
111
|
+
// 1. Time series by date
|
|
112
|
+
report(propertyId, token, {
|
|
113
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
114
|
+
dimensions: [{ name: "date" }],
|
|
115
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }, { name: "screenPageViews" }],
|
|
116
|
+
orderBys: [{ dimension: { dimensionName: "date" }, desc: false }]
|
|
117
|
+
}),
|
|
118
|
+
// 2. Hourly traffic (today only)
|
|
119
|
+
report(propertyId, token, {
|
|
120
|
+
dateRanges: [{ startDate: "today", endDate: "today" }],
|
|
121
|
+
dimensions: [{ name: "hour" }],
|
|
122
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }],
|
|
123
|
+
orderBys: [{ dimension: { dimensionName: "hour" }, desc: false }]
|
|
124
|
+
}),
|
|
125
|
+
// 3. Top pages
|
|
126
|
+
report(propertyId, token, {
|
|
127
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
128
|
+
dimensions: [{ name: "pagePath" }],
|
|
129
|
+
metrics: [{ name: "screenPageViews" }, { name: "totalUsers" }, { name: "averageSessionDuration" }],
|
|
130
|
+
orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
|
|
131
|
+
limit: 15
|
|
132
|
+
}),
|
|
133
|
+
// 4. Top landing pages
|
|
134
|
+
report(propertyId, token, {
|
|
135
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
136
|
+
dimensions: [{ name: "landingPage" }],
|
|
137
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }, { name: "bounceRate" }],
|
|
138
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
139
|
+
limit: 10
|
|
140
|
+
}),
|
|
141
|
+
// 5. Device category
|
|
142
|
+
report(propertyId, token, {
|
|
143
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
144
|
+
dimensions: [{ name: "deviceCategory" }],
|
|
145
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }]
|
|
146
|
+
}),
|
|
147
|
+
// 6. Browser breakdown
|
|
148
|
+
report(propertyId, token, {
|
|
149
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
150
|
+
dimensions: [{ name: "browser" }],
|
|
151
|
+
metrics: [{ name: "sessions" }],
|
|
152
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
153
|
+
limit: 8
|
|
154
|
+
}),
|
|
155
|
+
// 7. Operating system
|
|
156
|
+
report(propertyId, token, {
|
|
157
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
158
|
+
dimensions: [{ name: "operatingSystem" }],
|
|
159
|
+
metrics: [{ name: "sessions" }],
|
|
160
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
161
|
+
limit: 8
|
|
162
|
+
}),
|
|
163
|
+
// 8. Top countries
|
|
164
|
+
report(propertyId, token, {
|
|
165
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
166
|
+
dimensions: [{ name: "country" }],
|
|
167
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }],
|
|
168
|
+
orderBys: [{ metric: { metricName: "totalUsers" }, desc: true }],
|
|
169
|
+
limit: 10
|
|
170
|
+
}),
|
|
171
|
+
// 9. Top cities
|
|
172
|
+
report(propertyId, token, {
|
|
173
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
174
|
+
dimensions: [{ name: "city" }, { name: "country" }],
|
|
175
|
+
metrics: [{ name: "totalUsers" }, { name: "sessions" }],
|
|
176
|
+
orderBys: [{ metric: { metricName: "totalUsers" }, desc: true }],
|
|
177
|
+
limit: 10
|
|
178
|
+
}),
|
|
179
|
+
// 10. Traffic sources
|
|
180
|
+
report(propertyId, token, {
|
|
181
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
182
|
+
dimensions: [{ name: "sessionSource" }, { name: "sessionMedium" }],
|
|
183
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
184
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
185
|
+
limit: 10
|
|
186
|
+
}),
|
|
187
|
+
// 11. Channel grouping
|
|
188
|
+
report(propertyId, token, {
|
|
189
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
190
|
+
dimensions: [{ name: "sessionDefaultChannelGroup" }],
|
|
191
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }, { name: "engagementRate" }],
|
|
192
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }]
|
|
193
|
+
}),
|
|
194
|
+
// 12. New vs returning users
|
|
195
|
+
report(propertyId, token, {
|
|
196
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
197
|
+
dimensions: [{ name: "newVsReturning" }],
|
|
198
|
+
metrics: [{ name: "totalUsers" }]
|
|
199
|
+
}),
|
|
200
|
+
// 13. Top events
|
|
201
|
+
report(propertyId, token, {
|
|
202
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
203
|
+
dimensions: [{ name: "eventName" }],
|
|
204
|
+
metrics: [{ name: "eventCount" }, { name: "totalUsers" }],
|
|
205
|
+
orderBys: [{ metric: { metricName: "eventCount" }, desc: true }],
|
|
206
|
+
limit: 15
|
|
207
|
+
}),
|
|
208
|
+
// 14. Top referrers
|
|
209
|
+
report(propertyId, token, {
|
|
210
|
+
dateRanges: [{ startDate, endDate: "today" }],
|
|
211
|
+
dimensions: [{ name: "pageReferrer" }],
|
|
212
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
213
|
+
orderBys: [{ metric: { metricName: "sessions" }, desc: true }],
|
|
214
|
+
limit: 10
|
|
215
|
+
}),
|
|
216
|
+
// 15. Real-time active users (last 30 min)
|
|
217
|
+
realtimeReport(propertyId, token, {
|
|
218
|
+
metrics: [{ name: "activeUsers" }]
|
|
219
|
+
})
|
|
220
|
+
]);
|
|
221
|
+
settled.forEach((s, i) => {
|
|
222
|
+
if (s.status === "rejected")
|
|
223
|
+
console.error(`[analytics] report[${i}] failed:`, s.reason);
|
|
224
|
+
});
|
|
225
|
+
const ok = (i) => {
|
|
226
|
+
const s = settled[i];
|
|
227
|
+
return s && s.status === "fulfilled" ? s.value : {};
|
|
228
|
+
};
|
|
229
|
+
return Response.json(
|
|
230
|
+
{
|
|
231
|
+
activeUsers: ok(15),
|
|
232
|
+
overview: ok(0),
|
|
233
|
+
timeSeries: ok(1),
|
|
234
|
+
hourlyToday: ok(2),
|
|
235
|
+
topPages: ok(3),
|
|
236
|
+
landingPages: ok(4),
|
|
237
|
+
devices: ok(5),
|
|
238
|
+
browsers: ok(6),
|
|
239
|
+
operatingSystems: ok(7),
|
|
240
|
+
countries: ok(8),
|
|
241
|
+
cities: ok(9),
|
|
242
|
+
trafficSources: ok(10),
|
|
243
|
+
channels: ok(11),
|
|
244
|
+
newVsReturning: ok(12),
|
|
245
|
+
topEvents: ok(13),
|
|
246
|
+
referrers: ok(14)
|
|
247
|
+
},
|
|
248
|
+
{ headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60" } }
|
|
249
|
+
);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
252
|
+
console.error("[analytics] Unhandled error:", message);
|
|
253
|
+
return Response.json({ error: message }, { status: 500 });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
257
|
+
0 && (module.exports = {
|
|
258
|
+
GET
|
|
259
|
+
});
|
|
260
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/index.ts"],"sourcesContent":["import {SignJWT, importPKCS8} from 'jose'\n\nconst GA_TOKEN_URL = 'https://oauth2.googleapis.com/token'\nconst GA_API_BASE = 'https://analyticsdata.googleapis.com/v1beta'\nconst GA_SCOPE = 'https://www.googleapis.com/auth/analytics.readonly'\n\nlet cachedToken: {value: string; expiresAt: number} | null = null\n\nasync function getAccessToken(): Promise<string> {\n if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value\n\n const clientEmail = process.env.GA_SERVICE_ACCOUNT_EMAIL\n const privateKeyRaw = process.env.GA_PRIVATE_KEY\n if (!clientEmail || !privateKeyRaw)\n throw new Error('Missing GA_SERVICE_ACCOUNT_EMAIL or GA_PRIVATE_KEY env vars')\n\n const privateKey = privateKeyRaw.replace(/\\\\n/g, '\\n')\n const now = Math.floor(Date.now() / 1000)\n const key = await importPKCS8(privateKey, 'RS256')\n\n const jwt = await new SignJWT({scope: GA_SCOPE})\n .setProtectedHeader({alg: 'RS256'})\n .setIssuedAt(now)\n .setExpirationTime(now + 3600)\n .setIssuer(clientEmail)\n .setAudience(GA_TOKEN_URL)\n .sign(key)\n\n const tokenRes = await fetch(GA_TOKEN_URL, {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }),\n })\n if (!tokenRes.ok) throw new Error(`Failed to obtain access token: ${await tokenRes.text()}`)\n\n const {access_token, expires_in} = await tokenRes.json()\n cachedToken = {value: access_token, expiresAt: Date.now() + (expires_in - 60) * 1000}\n return access_token\n}\n\nasync function report(\n propertyId: string,\n token: string,\n body: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runReport`, {\n method: 'POST',\n headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => null)\n throw new Error(\n (err as {error?: {message?: string}})?.error?.message ||\n `GA API error ${res.status}: ${res.statusText}`,\n )\n }\n return res.json()\n}\n\nasync function realtimeReport(\n propertyId: string,\n token: string,\n body: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const res = await fetch(`${GA_API_BASE}/properties/${propertyId}:runRealtimeReport`, {\n method: 'POST',\n headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},\n body: JSON.stringify(body),\n })\n if (!res.ok) return {}\n return res.json()\n}\n\n/**\n * Next.js App Router compatible GET handler.\n *\n * Usage in apps/web/src/app/api/analytics/route.ts:\n *\n * export { GET } from 'sanity-plugin-ga-dashboard/api'\n *\n * Required environment variables:\n * GA_PROPERTY_ID - Numeric GA4 property ID\n * GA_SERVICE_ACCOUNT_EMAIL - Service account client_email\n * GA_PRIVATE_KEY - Service account private_key\n */\nexport async function GET(request: Request): Promise<Response> {\n try {\n const propertyId = process.env.GA_PROPERTY_ID\n if (!propertyId)\n return Response.json({error: 'GA_PROPERTY_ID not configured'}, {status: 500})\n\n const {searchParams} = new URL(request.url)\n const dateRange = searchParams.get('range') || '30'\n const startDate = `${dateRange}daysAgo`\n\n let token: string\n try {\n token = await getAccessToken()\n } catch (authErr: unknown) {\n const msg = authErr instanceof Error ? authErr.message : String(authErr)\n console.error('[analytics] Auth error:', msg)\n return Response.json({error: `Auth failed: ${msg}`}, {status: 500})\n }\n\n const settled = await Promise.allSettled([\n // 0. Overview metrics\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n metrics: [\n {name: 'totalUsers'},\n {name: 'newUsers'},\n {name: 'sessions'},\n {name: 'screenPageViews'},\n {name: 'averageSessionDuration'},\n {name: 'bounceRate'},\n {name: 'engagedSessions'},\n {name: 'engagementRate'},\n {name: 'screenPageViewsPerSession'},\n {name: 'eventsPerSession'},\n ],\n }),\n // 1. Time series by date\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'date'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}, {name: 'screenPageViews'}],\n orderBys: [{dimension: {dimensionName: 'date'}, desc: false}],\n }),\n // 2. Hourly traffic (today only)\n report(propertyId, token, {\n dateRanges: [{startDate: 'today', endDate: 'today'}],\n dimensions: [{name: 'hour'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}],\n orderBys: [{dimension: {dimensionName: 'hour'}, desc: false}],\n }),\n // 3. Top pages\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'pagePath'}],\n metrics: [{name: 'screenPageViews'}, {name: 'totalUsers'}, {name: 'averageSessionDuration'}],\n orderBys: [{metric: {metricName: 'screenPageViews'}, desc: true}],\n limit: 15,\n }),\n // 4. Top landing pages\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'landingPage'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}, {name: 'bounceRate'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 10,\n }),\n // 5. Device category\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'deviceCategory'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}],\n }),\n // 6. Browser breakdown\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'browser'}],\n metrics: [{name: 'sessions'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 8,\n }),\n // 7. Operating system\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'operatingSystem'}],\n metrics: [{name: 'sessions'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 8,\n }),\n // 8. Top countries\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'country'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}],\n orderBys: [{metric: {metricName: 'totalUsers'}, desc: true}],\n limit: 10,\n }),\n // 9. Top cities\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'city'}, {name: 'country'}],\n metrics: [{name: 'totalUsers'}, {name: 'sessions'}],\n orderBys: [{metric: {metricName: 'totalUsers'}, desc: true}],\n limit: 10,\n }),\n // 10. Traffic sources\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'sessionSource'}, {name: 'sessionMedium'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 10,\n }),\n // 11. Channel grouping\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'sessionDefaultChannelGroup'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}, {name: 'engagementRate'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n }),\n // 12. New vs returning users\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'newVsReturning'}],\n metrics: [{name: 'totalUsers'}],\n }),\n // 13. Top events\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'eventName'}],\n metrics: [{name: 'eventCount'}, {name: 'totalUsers'}],\n orderBys: [{metric: {metricName: 'eventCount'}, desc: true}],\n limit: 15,\n }),\n // 14. Top referrers\n report(propertyId, token, {\n dateRanges: [{startDate, endDate: 'today'}],\n dimensions: [{name: 'pageReferrer'}],\n metrics: [{name: 'sessions'}, {name: 'totalUsers'}],\n orderBys: [{metric: {metricName: 'sessions'}, desc: true}],\n limit: 10,\n }),\n // 15. Real-time active users (last 30 min)\n realtimeReport(propertyId, token, {\n metrics: [{name: 'activeUsers'}],\n }),\n ])\n\n settled.forEach((s, i) => {\n if (s.status === 'rejected')\n console.error(`[analytics] report[${i}] failed:`, s.reason)\n })\n\n const ok = (i: number) => {\n const s = settled[i]\n return s && s.status === 'fulfilled' ? (s as PromiseFulfilledResult<Record<string, unknown>>).value : {}\n }\n\n return Response.json(\n {\n activeUsers: ok(15),\n overview: ok(0),\n timeSeries: ok(1),\n hourlyToday: ok(2),\n topPages: ok(3),\n landingPages: ok(4),\n devices: ok(5),\n browsers: ok(6),\n operatingSystems: ok(7),\n countries: ok(8),\n cities: ok(9),\n trafficSources: ok(10),\n channels: ok(11),\n newVsReturning: ok(12),\n topEvents: ok(13),\n referrers: ok(14),\n },\n {headers: {'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60'}},\n )\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n console.error('[analytics] Unhandled error:', message)\n return Response.json({error: message}, {status: 500})\n }\n}\n"],"mappings":"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","names":[]}
|