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.
@@ -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":[]}