@coursebuilder/analytics 1.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api/index.d.ts +158 -0
  3. package/dist/api/index.js +317 -0
  4. package/dist/api/index.js.map +1 -0
  5. package/dist/catalog.d.ts +14 -0
  6. package/dist/catalog.js +209 -0
  7. package/dist/catalog.js.map +1 -0
  8. package/dist/components/index.d.ts +172 -0
  9. package/dist/components/index.js +1258 -0
  10. package/dist/components/index.js.map +1 -0
  11. package/dist/engine.d.ts +20 -0
  12. package/dist/engine.js +350 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +353 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/providers/database.d.ts +79 -0
  18. package/dist/providers/database.js +533 -0
  19. package/dist/providers/database.js.map +1 -0
  20. package/dist/providers/derived.d.ts +45 -0
  21. package/dist/providers/derived.js +32 -0
  22. package/dist/providers/derived.js.map +1 -0
  23. package/dist/providers/ga4.d.ts +43 -0
  24. package/dist/providers/ga4.js +220 -0
  25. package/dist/providers/ga4.js.map +1 -0
  26. package/dist/providers/index.d.ts +8 -0
  27. package/dist/providers/index.js +1239 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/mux.d.ts +103 -0
  30. package/dist/providers/mux.js +241 -0
  31. package/dist/providers/mux.js.map +1 -0
  32. package/dist/providers/survey.d.ts +102 -0
  33. package/dist/providers/survey.js +233 -0
  34. package/dist/providers/survey.js.map +1 -0
  35. package/dist/types.d.ts +303 -0
  36. package/dist/types.js +1 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +101 -0
  39. package/src/api/catalog-handler.ts +321 -0
  40. package/src/api/index.ts +4 -0
  41. package/src/api/token-handler.ts +71 -0
  42. package/src/catalog.ts +223 -0
  43. package/src/components/country-chart.tsx +114 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/omnibus-dashboard.tsx +1460 -0
  46. package/src/components/revenue-chart.tsx +251 -0
  47. package/src/components/use-chart-colors.ts +75 -0
  48. package/src/engine.ts +201 -0
  49. package/src/index.ts +7 -0
  50. package/src/providers/database.ts +795 -0
  51. package/src/providers/derived.ts +79 -0
  52. package/src/providers/ga4.ts +173 -0
  53. package/src/providers/index.ts +44 -0
  54. package/src/providers/mux.ts +438 -0
  55. package/src/providers/survey.ts +487 -0
  56. package/src/types.ts +333 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Skill Recordings Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,158 @@
1
+ import { QueryOptions, QueryResult, SurfaceName, SurfaceMap, AnalyticsRange as AnalyticsRange$1, RevenueSummary, ConversionFunnel, AttributionCoverage, TrafficOverview, YouTubeChannelOverview, TrafficRevenueCorrelation, TrafficDaily, RevenueDaily, YouTubeRevenueCorrelation, YouTubeDaily, SurveySummary, EmailRevenueOverview, SurveyRevenueCorrelation, RevenueByProduct, RevenueByCountry, RecentPurchase, AttributionCount, ShortlinkPerformance, RevenueBySource, ContentCorrelation, TopPage, TrafficSource, YouTubeVideoPerformance, YouTubeTrafficSource, SurveyListItem, SurveyResponsesDaily, SurveyQuestionBreakdown, SurveyResponseRow } from '../types.js';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { SurfaceEntry } from '../catalog.js';
4
+
5
+ type AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all';
6
+ interface CatalogHandlerDeps {
7
+ engine: {
8
+ query: (surface: string, options?: QueryOptions) => Promise<QueryResult<SurfaceName>>;
9
+ getCatalog: () => SurfaceEntry[];
10
+ };
11
+ checkAccess: (request: Request) => Promise<{
12
+ authorized: boolean;
13
+ user?: Record<string, unknown> | null;
14
+ authMethod?: string;
15
+ }>;
16
+ logger?: {
17
+ info: (message: string, data?: Record<string, unknown>) => unknown;
18
+ warn: (message: string, data?: Record<string, unknown>) => unknown;
19
+ error: (message: string, data?: Record<string, unknown>) => unknown;
20
+ };
21
+ appName?: string;
22
+ baseUrl?: string;
23
+ }
24
+
25
+ /**
26
+ * Creates a Next.js App Router GET handler that serves a HATEOAS-style
27
+ * analytics catalog and surface query API.
28
+ *
29
+ * @param deps - Handler dependencies including engine, access check, and
30
+ * optional logger and app metadata
31
+ * @returns An object with `GET` and `OPTIONS` Next.js route handlers
32
+ */
33
+ declare function createAnalyticsCatalogHandler(deps: CatalogHandlerDeps): {
34
+ GET: (request: NextRequest) => Promise<NextResponse<{
35
+ ok: boolean;
36
+ endpoint: string;
37
+ error: {
38
+ message: string;
39
+ code: string;
40
+ };
41
+ fix: string;
42
+ next_actions: {
43
+ command: string;
44
+ description: string;
45
+ }[];
46
+ }> | NextResponse<{
47
+ ok: boolean;
48
+ endpoint: string;
49
+ description: string;
50
+ notes: string[];
51
+ surfaces: SurfaceEntry[];
52
+ _links: {
53
+ self: {
54
+ href: string;
55
+ };
56
+ };
57
+ next_actions: {
58
+ command: string;
59
+ description: string;
60
+ params: {
61
+ surface: {
62
+ required: boolean;
63
+ enum: (keyof SurfaceMap)[];
64
+ description: string;
65
+ };
66
+ range: {
67
+ default: string;
68
+ enum: AnalyticsRange[];
69
+ description: string;
70
+ };
71
+ limit: {
72
+ default: string;
73
+ description: string;
74
+ };
75
+ };
76
+ }[];
77
+ }> | NextResponse<{
78
+ ok: boolean;
79
+ endpoint: string;
80
+ surface: string;
81
+ range: AnalyticsRange$1;
82
+ description: string;
83
+ data: RevenueSummary | ConversionFunnel | AttributionCoverage | TrafficOverview | YouTubeChannelOverview | TrafficRevenueCorrelation | TrafficDaily[] | RevenueDaily[] | YouTubeRevenueCorrelation | YouTubeDaily[] | SurveySummary | EmailRevenueOverview | SurveyRevenueCorrelation | RevenueByProduct[] | RevenueByCountry[] | RecentPurchase[] | AttributionCount[] | ShortlinkPerformance[] | RevenueBySource[] | ContentCorrelation[] | TopPage[] | TrafficSource[] | YouTubeVideoPerformance[] | YouTubeTrafficSource[] | SurveyListItem[] | SurveyResponsesDaily[] | SurveyQuestionBreakdown[] | SurveyResponseRow[];
84
+ meta: {
85
+ totalRows: number;
86
+ truncated: boolean;
87
+ queryTimeMs: number;
88
+ };
89
+ _links: {
90
+ self: {
91
+ href: string;
92
+ };
93
+ catalog: {
94
+ href: string;
95
+ };
96
+ };
97
+ next_actions: {
98
+ command: string;
99
+ description: string;
100
+ params: {
101
+ range: {
102
+ value: AnalyticsRange;
103
+ enum: AnalyticsRange[];
104
+ };
105
+ };
106
+ }[];
107
+ }>>;
108
+ OPTIONS: () => NextResponse<{}>;
109
+ };
110
+
111
+ interface TokenHandlerDeps {
112
+ db: {
113
+ insert: (table: any) => {
114
+ values: (data: any) => Promise<any>;
115
+ };
116
+ };
117
+ deviceAccessToken: unknown;
118
+ checkAccess: (request: Request) => Promise<{
119
+ authorized: boolean;
120
+ userId?: string;
121
+ user?: {
122
+ email?: string;
123
+ [key: string]: unknown;
124
+ } | null;
125
+ }>;
126
+ ttlHours?: number;
127
+ logger?: {
128
+ info: (message: string, data?: Record<string, unknown>) => unknown;
129
+ warn: (message: string, data?: Record<string, unknown>) => unknown;
130
+ error: (message: string, data?: Record<string, unknown>) => unknown;
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Creates a Next.js App Router POST handler that generates short-lived
136
+ * device access tokens for analytics API authentication.
137
+ *
138
+ * Tokens are stored with only `token` and `userId`. Expiry is **not**
139
+ * stored in the database — it is enforced at lookup time by comparing
140
+ * the row's `createdAt` timestamp against the configured TTL. Each
141
+ * app's `getUserAbilityForRequest` must enforce this check.
142
+ *
143
+ * @param deps - Handler dependencies including db, deviceAccessToken schema
144
+ * table, an access-check function, and optional TTL and logger overrides
145
+ * @returns An object with a `POST` Next.js route handler
146
+ */
147
+ declare function createTokenHandler(deps: TokenHandlerDeps): {
148
+ POST: (request: NextRequest) => Promise<NextResponse<{
149
+ error: string;
150
+ }> | NextResponse<{
151
+ token: string;
152
+ ttl: string;
153
+ ttlLabel: string;
154
+ expiresAt: string;
155
+ }>>;
156
+ };
157
+
158
+ export { type CatalogHandlerDeps, type TokenHandlerDeps, createAnalyticsCatalogHandler, createTokenHandler };
@@ -0,0 +1,317 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/api/catalog-handler.ts
5
+ import { NextResponse } from "next/server";
6
+ var VALID_RANGES = /* @__PURE__ */ new Set([
7
+ "24h",
8
+ "7d",
9
+ "30d",
10
+ "90d",
11
+ "all"
12
+ ]);
13
+ var RANGE_OPTIONS = [
14
+ "24h",
15
+ "7d",
16
+ "30d",
17
+ "90d",
18
+ "all"
19
+ ];
20
+ var CATEGORY_SUGGESTIONS = {
21
+ revenue: [
22
+ "revenue/daily",
23
+ "revenue/products",
24
+ "attribution/sources",
25
+ "correlation/traffic-revenue"
26
+ ],
27
+ attribution: [
28
+ "attribution/funnel",
29
+ "attribution/sources",
30
+ "attribution/coverage",
31
+ "correlation/traffic-revenue"
32
+ ],
33
+ traffic: [
34
+ "traffic/daily",
35
+ "traffic/sources",
36
+ "correlation/traffic-revenue",
37
+ "correlation/youtube-revenue"
38
+ ],
39
+ youtube: [
40
+ "youtube/videos",
41
+ "youtube/daily",
42
+ "youtube/sources",
43
+ "correlation/youtube-revenue"
44
+ ],
45
+ correlation: [
46
+ "summary",
47
+ "attribution/funnel",
48
+ "youtube",
49
+ "correlation/survey-revenue"
50
+ ],
51
+ survey: [
52
+ "surveys",
53
+ "surveys/list",
54
+ "surveys/daily",
55
+ "surveys/questions",
56
+ "surveys/responses"
57
+ ]
58
+ };
59
+ var corsHeaders = {
60
+ "Access-Control-Allow-Origin": "*",
61
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
62
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
63
+ };
64
+ function parseRange(raw) {
65
+ if (raw && VALID_RANGES.has(raw)) {
66
+ return raw;
67
+ }
68
+ return "30d";
69
+ }
70
+ __name(parseRange, "parseRange");
71
+ function getMeta(data, queryTimeMs, truncated) {
72
+ return {
73
+ totalRows: Array.isArray(data) ? data.length : 1,
74
+ truncated,
75
+ queryTimeMs
76
+ };
77
+ }
78
+ __name(getMeta, "getMeta");
79
+ function createAnalyticsCatalogHandler(deps) {
80
+ const { engine, checkAccess, logger, appName, baseUrl } = deps;
81
+ const catalog = engine.getCatalog();
82
+ const catalogByName = Object.fromEntries(catalog.map((entry) => [
83
+ entry.name,
84
+ entry
85
+ ]));
86
+ function buildContextualNextActions(surface, range, endpointPath) {
87
+ const entry = catalogByName[surface];
88
+ if (!entry)
89
+ return [];
90
+ const suggestions = CATEGORY_SUGGESTIONS[entry.category] ?? [];
91
+ return suggestions.filter((name) => name !== surface).slice(0, 4).map((name) => ({
92
+ command: `GET ${endpointPath}?surface=${name}&range=<range>`,
93
+ description: catalogByName[name]?.description ?? name,
94
+ params: {
95
+ range: {
96
+ value: range,
97
+ enum: RANGE_OPTIONS
98
+ }
99
+ }
100
+ }));
101
+ }
102
+ __name(buildContextualNextActions, "buildContextualNextActions");
103
+ const OPTIONS = /* @__PURE__ */ __name(() => NextResponse.json({}, {
104
+ headers: corsHeaders
105
+ }), "OPTIONS");
106
+ const GET = /* @__PURE__ */ __name(async (request) => {
107
+ const requestUrl = new URL(request.url);
108
+ const resolvedBase = baseUrl ?? `${requestUrl.protocol}//${requestUrl.host}`;
109
+ const endpointPath = `${requestUrl.pathname}`;
110
+ const appLabel = appName ?? "Analytics";
111
+ const access = await checkAccess(request);
112
+ if (!access.authorized) {
113
+ if (logger) {
114
+ void logger.warn("api.analytics.access-denied", {
115
+ userId: access.user?.id ?? null,
116
+ email: access.user?.email ?? null,
117
+ authMethod: access.authMethod ?? "unknown",
118
+ hasAuthorization: false
119
+ });
120
+ }
121
+ return NextResponse.json({
122
+ ok: false,
123
+ endpoint: endpointPath,
124
+ error: {
125
+ message: "Unauthorized",
126
+ code: "AUTH_REQUIRED"
127
+ },
128
+ fix: "Authenticate with an admin device token or an admin session cookie.",
129
+ next_actions: [
130
+ {
131
+ command: "GET /api/coursebuilder/devices",
132
+ description: "Start device verification flow to obtain a Bearer token"
133
+ },
134
+ {
135
+ command: "GET /login",
136
+ description: "Log in as an admin to use session-based auth"
137
+ }
138
+ ]
139
+ }, {
140
+ status: 401,
141
+ headers: corsHeaders
142
+ });
143
+ }
144
+ const { searchParams } = requestUrl;
145
+ const rawSurface = searchParams.get("surface");
146
+ if (!rawSurface) {
147
+ return NextResponse.json({
148
+ ok: true,
149
+ endpoint: endpointPath,
150
+ description: `${appLabel} analytics \u2014 revenue, attribution, traffic, YouTube, and content correlation`,
151
+ notes: [
152
+ "YouTube surfaces are useful for correlation/content analysis but lag by about 48 hours."
153
+ ],
154
+ surfaces: catalog,
155
+ _links: {
156
+ self: {
157
+ href: `${resolvedBase}${endpointPath}`
158
+ }
159
+ },
160
+ next_actions: [
161
+ {
162
+ command: `GET ${endpointPath}?surface=<surface>&range=<range>&limit=<limit>`,
163
+ description: "Query a specific analytics surface",
164
+ params: {
165
+ surface: {
166
+ required: true,
167
+ enum: catalog.map((entry) => entry.name),
168
+ description: "Analytics surface to query"
169
+ },
170
+ range: {
171
+ default: "30d",
172
+ enum: RANGE_OPTIONS,
173
+ description: "Time range"
174
+ },
175
+ limit: {
176
+ default: "20",
177
+ description: "Max rows for surfaces that support it (max 100)"
178
+ }
179
+ }
180
+ }
181
+ ]
182
+ }, {
183
+ headers: corsHeaders
184
+ });
185
+ }
186
+ if (!(rawSurface in catalogByName)) {
187
+ return NextResponse.json({
188
+ ok: false,
189
+ endpoint: endpointPath,
190
+ error: {
191
+ message: `Unknown surface: ${rawSurface}`,
192
+ code: "INVALID_SURFACE"
193
+ },
194
+ fix: `Hit GET ${endpointPath} with no params for the full surface catalog.`,
195
+ next_actions: [
196
+ {
197
+ command: `GET ${endpointPath}`,
198
+ description: "Browse the full analytics surface catalog"
199
+ }
200
+ ]
201
+ }, {
202
+ status: 400,
203
+ headers: corsHeaders
204
+ });
205
+ }
206
+ const surface = rawSurface;
207
+ const range = parseRange(searchParams.get("range"));
208
+ const limit = Math.min(Number(searchParams.get("limit") ?? 20), 100);
209
+ if (logger) {
210
+ logger.info("api.analytics.query", {
211
+ userId: access.user?.id ?? null,
212
+ email: access.user?.email ?? null,
213
+ authMethod: access.authMethod ?? "unknown",
214
+ surface,
215
+ range,
216
+ limit
217
+ });
218
+ }
219
+ const result = await engine.query(surface, {
220
+ range,
221
+ limit
222
+ });
223
+ if (!result.ok) {
224
+ if (logger) {
225
+ logger.error("api.analytics.error", {
226
+ userId: access.user?.id ?? null,
227
+ surface,
228
+ range,
229
+ code: result.error.code,
230
+ error: result.error.message
231
+ });
232
+ }
233
+ return NextResponse.json({
234
+ ok: false,
235
+ endpoint: endpointPath,
236
+ surface,
237
+ error: result.error,
238
+ fix: result.fix,
239
+ next_actions: buildContextualNextActions(surface, range, endpointPath)
240
+ }, {
241
+ status: result.error.code.endsWith("_UNAVAILABLE") ? 503 : 500,
242
+ headers: corsHeaders
243
+ });
244
+ }
245
+ return NextResponse.json({
246
+ ok: true,
247
+ endpoint: endpointPath,
248
+ surface,
249
+ range: result.range,
250
+ description: catalogByName[surface]?.description,
251
+ data: result.data,
252
+ meta: getMeta(result.data, result.meta.queryTimeMs, result.meta.truncated),
253
+ _links: {
254
+ self: {
255
+ href: `${resolvedBase}${endpointPath}?surface=${surface}&range=${range}`
256
+ },
257
+ catalog: {
258
+ href: `${resolvedBase}${endpointPath}`
259
+ }
260
+ },
261
+ next_actions: buildContextualNextActions(surface, range, endpointPath)
262
+ }, {
263
+ headers: corsHeaders
264
+ });
265
+ }, "GET");
266
+ return {
267
+ GET,
268
+ OPTIONS
269
+ };
270
+ }
271
+ __name(createAnalyticsCatalogHandler, "createAnalyticsCatalogHandler");
272
+
273
+ // src/api/token-handler.ts
274
+ import { NextResponse as NextResponse2 } from "next/server";
275
+ function createTokenHandler(deps) {
276
+ const { db, deviceAccessToken, checkAccess, logger } = deps;
277
+ const ttlHours = deps.ttlHours ?? 24;
278
+ const ttlLabel = `${ttlHours} hours`;
279
+ const POST = /* @__PURE__ */ __name(async (request) => {
280
+ const access = await checkAccess(request);
281
+ if (!access.authorized || !access.userId) {
282
+ return NextResponse2.json({
283
+ error: "Unauthorized"
284
+ }, {
285
+ status: 401
286
+ });
287
+ }
288
+ const userId = access.userId;
289
+ const token = crypto.randomUUID();
290
+ await db.insert(deviceAccessToken).values({
291
+ token,
292
+ userId
293
+ });
294
+ if (logger) {
295
+ void logger.info("api.analytics.token-generated", {
296
+ userId,
297
+ email: access.user?.email ?? null,
298
+ ttlHours
299
+ });
300
+ }
301
+ return NextResponse2.json({
302
+ token,
303
+ ttl: `${ttlHours}h`,
304
+ ttlLabel,
305
+ expiresAt: new Date(Date.now() + ttlHours * 60 * 60 * 1e3).toISOString()
306
+ });
307
+ }, "POST");
308
+ return {
309
+ POST
310
+ };
311
+ }
312
+ __name(createTokenHandler, "createTokenHandler");
313
+ export {
314
+ createAnalyticsCatalogHandler,
315
+ createTokenHandler
316
+ };
317
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/catalog-handler.ts","../../src/api/token-handler.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server'\n\nimport type { SurfaceEntry } from '../catalog'\nimport type { QueryOptions, QueryResult, SurfaceName } from '../types'\n\ntype AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all'\n\ninterface CatalogHandlerDeps {\n\tengine: {\n\t\tquery: (\n\t\t\tsurface: string,\n\t\t\toptions?: QueryOptions,\n\t\t) => Promise<QueryResult<SurfaceName>>\n\t\tgetCatalog: () => SurfaceEntry[]\n\t}\n\tcheckAccess: (request: Request) => Promise<{\n\t\tauthorized: boolean\n\t\tuser?: Record<string, unknown> | null\n\t\tauthMethod?: string\n\t}>\n\tlogger?: {\n\t\tinfo: (message: string, data?: Record<string, unknown>) => unknown\n\t\twarn: (message: string, data?: Record<string, unknown>) => unknown\n\t\terror: (message: string, data?: Record<string, unknown>) => unknown\n\t}\n\tappName?: string\n\tbaseUrl?: string\n}\n\nexport type { CatalogHandlerDeps }\n\nconst VALID_RANGES = new Set<AnalyticsRange>(['24h', '7d', '30d', '90d', 'all'])\nconst RANGE_OPTIONS: AnalyticsRange[] = ['24h', '7d', '30d', '90d', 'all']\n\nconst CATEGORY_SUGGESTIONS: Record<string, string[]> = {\n\trevenue: [\n\t\t'revenue/daily',\n\t\t'revenue/products',\n\t\t'attribution/sources',\n\t\t'correlation/traffic-revenue',\n\t],\n\tattribution: [\n\t\t'attribution/funnel',\n\t\t'attribution/sources',\n\t\t'attribution/coverage',\n\t\t'correlation/traffic-revenue',\n\t],\n\ttraffic: [\n\t\t'traffic/daily',\n\t\t'traffic/sources',\n\t\t'correlation/traffic-revenue',\n\t\t'correlation/youtube-revenue',\n\t],\n\tyoutube: [\n\t\t'youtube/videos',\n\t\t'youtube/daily',\n\t\t'youtube/sources',\n\t\t'correlation/youtube-revenue',\n\t],\n\tcorrelation: [\n\t\t'summary',\n\t\t'attribution/funnel',\n\t\t'youtube',\n\t\t'correlation/survey-revenue',\n\t],\n\tsurvey: [\n\t\t'surveys',\n\t\t'surveys/list',\n\t\t'surveys/daily',\n\t\t'surveys/questions',\n\t\t'surveys/responses',\n\t],\n}\n\nconst corsHeaders = {\n\t'Access-Control-Allow-Origin': '*',\n\t'Access-Control-Allow-Methods': 'GET, OPTIONS',\n\t'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n}\n\nfunction parseRange(raw?: string | null): AnalyticsRange {\n\tif (raw && VALID_RANGES.has(raw as AnalyticsRange)) {\n\t\treturn raw as AnalyticsRange\n\t}\n\n\treturn '30d'\n}\n\nfunction getMeta(data: unknown, queryTimeMs: number, truncated: boolean) {\n\treturn {\n\t\ttotalRows: Array.isArray(data) ? data.length : 1,\n\t\ttruncated,\n\t\tqueryTimeMs,\n\t}\n}\n\n/**\n * Creates a Next.js App Router GET handler that serves a HATEOAS-style\n * analytics catalog and surface query API.\n *\n * @param deps - Handler dependencies including engine, access check, and\n * optional logger and app metadata\n * @returns An object with `GET` and `OPTIONS` Next.js route handlers\n */\nexport function createAnalyticsCatalogHandler(deps: CatalogHandlerDeps) {\n\tconst { engine, checkAccess, logger, appName, baseUrl } = deps\n\n\tconst catalog = engine.getCatalog()\n\tconst catalogByName = Object.fromEntries(\n\t\tcatalog.map((entry) => [entry.name, entry]),\n\t) as Record<string, SurfaceEntry>\n\n\tfunction buildContextualNextActions(\n\t\tsurface: string,\n\t\trange: AnalyticsRange,\n\t\tendpointPath: string,\n\t) {\n\t\tconst entry = catalogByName[surface]\n\t\tif (!entry) return []\n\t\tconst suggestions = CATEGORY_SUGGESTIONS[entry.category] ?? []\n\n\t\treturn suggestions\n\t\t\t.filter((name) => name !== surface)\n\t\t\t.slice(0, 4)\n\t\t\t.map((name) => ({\n\t\t\t\tcommand: `GET ${endpointPath}?surface=${name}&range=<range>`,\n\t\t\t\tdescription: catalogByName[name]?.description ?? name,\n\t\t\t\tparams: {\n\t\t\t\t\trange: {\n\t\t\t\t\t\tvalue: range,\n\t\t\t\t\t\tenum: RANGE_OPTIONS,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t}\n\n\tconst OPTIONS = () => NextResponse.json({}, { headers: corsHeaders })\n\n\tconst GET = async (request: NextRequest) => {\n\t\tconst requestUrl = new URL(request.url)\n\t\tconst resolvedBase = baseUrl ?? `${requestUrl.protocol}//${requestUrl.host}`\n\t\tconst endpointPath = `${requestUrl.pathname}`\n\t\tconst appLabel = appName ?? 'Analytics'\n\n\t\tconst access = await checkAccess(request)\n\n\t\tif (!access.authorized) {\n\t\t\tif (logger) {\n\t\t\t\tvoid logger.warn('api.analytics.access-denied', {\n\t\t\t\t\tuserId: access.user?.id ?? null,\n\t\t\t\t\temail: access.user?.email ?? null,\n\t\t\t\t\tauthMethod: access.authMethod ?? 'unknown',\n\t\t\t\t\thasAuthorization: false,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn NextResponse.json(\n\t\t\t\t{\n\t\t\t\t\tok: false,\n\t\t\t\t\tendpoint: endpointPath,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tmessage: 'Unauthorized',\n\t\t\t\t\t\tcode: 'AUTH_REQUIRED',\n\t\t\t\t\t},\n\t\t\t\t\tfix: 'Authenticate with an admin device token or an admin session cookie.',\n\t\t\t\t\tnext_actions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcommand: 'GET /api/coursebuilder/devices',\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t'Start device verification flow to obtain a Bearer token',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcommand: 'GET /login',\n\t\t\t\t\t\t\tdescription: 'Log in as an admin to use session-based auth',\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ status: 401, headers: corsHeaders },\n\t\t\t)\n\t\t}\n\n\t\tconst { searchParams } = requestUrl\n\t\tconst rawSurface = searchParams.get('surface')\n\n\t\tif (!rawSurface) {\n\t\t\treturn NextResponse.json(\n\t\t\t\t{\n\t\t\t\t\tok: true,\n\t\t\t\t\tendpoint: endpointPath,\n\t\t\t\t\tdescription: `${appLabel} analytics — revenue, attribution, traffic, YouTube, and content correlation`,\n\t\t\t\t\tnotes: [\n\t\t\t\t\t\t'YouTube surfaces are useful for correlation/content analysis but lag by about 48 hours.',\n\t\t\t\t\t],\n\t\t\t\t\tsurfaces: catalog,\n\t\t\t\t\t_links: {\n\t\t\t\t\t\tself: { href: `${resolvedBase}${endpointPath}` },\n\t\t\t\t\t},\n\t\t\t\t\tnext_actions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcommand: `GET ${endpointPath}?surface=<surface>&range=<range>&limit=<limit>`,\n\t\t\t\t\t\t\tdescription: 'Query a specific analytics surface',\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tsurface: {\n\t\t\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\t\t\tenum: catalog.map((entry) => entry.name),\n\t\t\t\t\t\t\t\t\tdescription: 'Analytics surface to query',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\trange: {\n\t\t\t\t\t\t\t\t\tdefault: '30d',\n\t\t\t\t\t\t\t\t\tenum: RANGE_OPTIONS,\n\t\t\t\t\t\t\t\t\tdescription: 'Time range',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tlimit: {\n\t\t\t\t\t\t\t\t\tdefault: '20',\n\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t'Max rows for surfaces that support it (max 100)',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ headers: corsHeaders },\n\t\t\t)\n\t\t}\n\n\t\tif (!(rawSurface in catalogByName)) {\n\t\t\treturn NextResponse.json(\n\t\t\t\t{\n\t\t\t\t\tok: false,\n\t\t\t\t\tendpoint: endpointPath,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tmessage: `Unknown surface: ${rawSurface}`,\n\t\t\t\t\t\tcode: 'INVALID_SURFACE',\n\t\t\t\t\t},\n\t\t\t\t\tfix: `Hit GET ${endpointPath} with no params for the full surface catalog.`,\n\t\t\t\t\tnext_actions: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcommand: `GET ${endpointPath}`,\n\t\t\t\t\t\t\tdescription: 'Browse the full analytics surface catalog',\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ status: 400, headers: corsHeaders },\n\t\t\t)\n\t\t}\n\n\t\tconst surface = rawSurface\n\t\tconst range = parseRange(searchParams.get('range'))\n\t\tconst limit = Math.min(Number(searchParams.get('limit') ?? 20), 100)\n\n\t\tif (logger) {\n\t\t\tlogger.info('api.analytics.query', {\n\t\t\t\tuserId: access.user?.id ?? null,\n\t\t\t\temail: access.user?.email ?? null,\n\t\t\t\tauthMethod: access.authMethod ?? 'unknown',\n\t\t\t\tsurface,\n\t\t\t\trange,\n\t\t\t\tlimit,\n\t\t\t})\n\t\t}\n\n\t\tconst result = await engine.query(surface, { range, limit })\n\n\t\tif (!result.ok) {\n\t\t\tif (logger) {\n\t\t\t\tlogger.error('api.analytics.error', {\n\t\t\t\t\tuserId: access.user?.id ?? null,\n\t\t\t\t\tsurface,\n\t\t\t\t\trange,\n\t\t\t\t\tcode: result.error.code,\n\t\t\t\t\terror: result.error.message,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn NextResponse.json(\n\t\t\t\t{\n\t\t\t\t\tok: false,\n\t\t\t\t\tendpoint: endpointPath,\n\t\t\t\t\tsurface,\n\t\t\t\t\terror: result.error,\n\t\t\t\t\tfix: result.fix,\n\t\t\t\t\tnext_actions: buildContextualNextActions(\n\t\t\t\t\t\tsurface,\n\t\t\t\t\t\trange,\n\t\t\t\t\t\tendpointPath,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: result.error.code.endsWith('_UNAVAILABLE') ? 503 : 500,\n\t\t\t\t\theaders: corsHeaders,\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\treturn NextResponse.json(\n\t\t\t{\n\t\t\t\tok: true,\n\t\t\t\tendpoint: endpointPath,\n\t\t\t\tsurface,\n\t\t\t\trange: result.range,\n\t\t\t\tdescription: catalogByName[surface]?.description,\n\t\t\t\tdata: result.data,\n\t\t\t\tmeta: getMeta(\n\t\t\t\t\tresult.data,\n\t\t\t\t\tresult.meta.queryTimeMs,\n\t\t\t\t\tresult.meta.truncated,\n\t\t\t\t),\n\t\t\t\t_links: {\n\t\t\t\t\tself: {\n\t\t\t\t\t\thref: `${resolvedBase}${endpointPath}?surface=${surface}&range=${range}`,\n\t\t\t\t\t},\n\t\t\t\t\tcatalog: { href: `${resolvedBase}${endpointPath}` },\n\t\t\t\t},\n\t\t\t\tnext_actions: buildContextualNextActions(surface, range, endpointPath),\n\t\t\t},\n\t\t\t{ headers: corsHeaders },\n\t\t)\n\t}\n\n\treturn { GET, OPTIONS }\n}\n","import { NextRequest, NextResponse } from 'next/server'\n\ninterface TokenHandlerDeps {\n\tdb: { insert: (table: any) => { values: (data: any) => Promise<any> } }\n\tdeviceAccessToken: unknown\n\tcheckAccess: (request: Request) => Promise<{\n\t\tauthorized: boolean\n\t\tuserId?: string\n\t\tuser?: { email?: string; [key: string]: unknown } | null\n\t}>\n\tttlHours?: number\n\tlogger?: {\n\t\tinfo: (message: string, data?: Record<string, unknown>) => unknown\n\t\twarn: (message: string, data?: Record<string, unknown>) => unknown\n\t\terror: (message: string, data?: Record<string, unknown>) => unknown\n\t}\n}\n\nexport type { TokenHandlerDeps }\n\n/**\n * Creates a Next.js App Router POST handler that generates short-lived\n * device access tokens for analytics API authentication.\n *\n * Tokens are stored with only `token` and `userId`. Expiry is **not**\n * stored in the database — it is enforced at lookup time by comparing\n * the row's `createdAt` timestamp against the configured TTL. Each\n * app's `getUserAbilityForRequest` must enforce this check.\n *\n * @param deps - Handler dependencies including db, deviceAccessToken schema\n * table, an access-check function, and optional TTL and logger overrides\n * @returns An object with a `POST` Next.js route handler\n */\nexport function createTokenHandler(deps: TokenHandlerDeps) {\n\tconst { db, deviceAccessToken, checkAccess, logger } = deps\n\tconst ttlHours = deps.ttlHours ?? 24\n\tconst ttlLabel = `${ttlHours} hours`\n\n\tconst POST = async (request: NextRequest) => {\n\t\tconst access = await checkAccess(request)\n\n\t\tif (!access.authorized || !access.userId) {\n\t\t\treturn NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\t\t}\n\n\t\tconst userId = access.userId\n\t\tconst token = crypto.randomUUID()\n\n\t\tawait db.insert(deviceAccessToken).values({\n\t\t\ttoken,\n\t\t\tuserId,\n\t\t})\n\n\t\tif (logger) {\n\t\t\tvoid logger.info('api.analytics.token-generated', {\n\t\t\t\tuserId,\n\t\t\t\temail: access.user?.email ?? null,\n\t\t\t\tttlHours,\n\t\t\t})\n\t\t}\n\n\t\treturn NextResponse.json({\n\t\t\ttoken,\n\t\t\tttl: `${ttlHours}h`,\n\t\t\tttlLabel,\n\t\t\texpiresAt: new Date(Date.now() + ttlHours * 60 * 60 * 1000).toISOString(),\n\t\t})\n\t}\n\n\treturn { POST }\n}\n"],"mappings":"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","names":["NextResponse","VALID_RANGES","Set","RANGE_OPTIONS","CATEGORY_SUGGESTIONS","revenue","attribution","traffic","youtube","correlation","survey","corsHeaders","parseRange","raw","has","getMeta","data","queryTimeMs","truncated","totalRows","Array","isArray","length","createAnalyticsCatalogHandler","deps","engine","checkAccess","logger","appName","baseUrl","catalog","getCatalog","catalogByName","Object","fromEntries","map","entry","name","buildContextualNextActions","surface","range","endpointPath","suggestions","category","filter","slice","command","description","params","value","enum","OPTIONS","NextResponse","json","headers","GET","request","requestUrl","URL","url","resolvedBase","protocol","host","pathname","appLabel","access","authorized","warn","userId","user","id","email","authMethod","hasAuthorization","ok","endpoint","error","message","code","fix","next_actions","status","searchParams","rawSurface","get","notes","surfaces","_links","self","href","required","default","limit","Math","min","Number","info","result","query","endsWith","meta","NextResponse","createTokenHandler","deps","db","deviceAccessToken","checkAccess","logger","ttlHours","ttlLabel","POST","request","access","authorized","userId","NextResponse","json","error","status","token","crypto","randomUUID","insert","values","info","email","user","ttl","expiresAt","Date","now","toISOString"]}
@@ -0,0 +1,14 @@
1
+ import { SurfaceMap, SurfaceName } from './types.js';
2
+
3
+ interface SurfaceEntry {
4
+ name: SurfaceName;
5
+ description: string;
6
+ category: 'revenue' | 'attribution' | 'traffic' | 'youtube' | 'correlation' | 'survey';
7
+ provider: 'database' | 'ga4' | 'youtube' | 'derived' | 'newsletter' | 'survey';
8
+ fn: string;
9
+ unavailableFix?: string;
10
+ }
11
+ declare const catalog: Record<SurfaceName, SurfaceEntry>;
12
+ declare const ANALYTICS_CATALOG: Record<keyof SurfaceMap, SurfaceEntry>;
13
+
14
+ export { ANALYTICS_CATALOG, type SurfaceEntry, catalog };