@coursebuilder/analytics 1.1.0 → 1.1.1
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.ts +22 -2
- package/dist/api/index.js +40 -5
- package/dist/api/index.js.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +43 -1
- package/dist/catalog.js.map +1 -1
- package/dist/components/index.d.ts +29 -0
- package/dist/components/index.js +91 -2
- package/dist/components/index.js.map +1 -1
- package/dist/engine.js +94 -6
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +94 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/database.d.ts +144 -2
- package/dist/providers/database.js +652 -20
- package/dist/providers/database.js.map +1 -1
- package/dist/providers/index.js +654 -22
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/survey.d.ts +1 -1
- package/dist/providers/survey.js +2 -2
- package/dist/providers/survey.js.map +1 -1
- package/dist/types.d.ts +151 -3
- package/package.json +5 -3
- package/src/api/catalog-handler.ts +44 -2
- package/src/api/token-handler.ts +3 -2
- package/src/catalog.ts +49 -1
- package/src/components/omnibus-dashboard.tsx +163 -0
- package/src/engine.ts +66 -6
- package/src/providers/attribution-recovery.test.ts +63 -0
- package/src/providers/attribution-recovery.ts +97 -0
- package/src/providers/database.ts +812 -42
- package/src/providers/survey.ts +3 -1
- package/src/types.ts +166 -2
package/dist/api/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
1
|
+
import { QueryOptions, QueryResult, SurfaceName, SurfaceMap, AnalyticsRange as AnalyticsRange$1, RevenueSummary, ConversionFunnel, CommerceLaneSummary, AttributionCoverage, TrafficOverview, YouTubeChannelOverview, TrafficRevenueCorrelation, TrafficDaily, RevenueDaily, YouTubeRevenueCorrelation, YouTubeDaily, SurveySummary, EmailRevenueOverview, SurveyRevenueCorrelation, ProductSurveyRevenueCorrelation, CheckoutAttributionReceipt, CheckoutSurveyFallbackReport, ValuePathSummary, RevenueByProduct, RevenueByCountry, RecentPurchase, AttributionCount, ShortlinkPerformance, RevenueBySource, ContentCorrelation, TopPage, TrafficSource, YouTubeVideoPerformance, YouTubeTrafficSource, SurveyListItem, SurveyResponsesDaily, SurveyQuestionBreakdown, SurveyResponseRow } from '../types.js';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { SurfaceEntry } from '../catalog.js';
|
|
4
4
|
|
|
@@ -72,6 +72,26 @@ declare function createAnalyticsCatalogHandler(deps: CatalogHandlerDeps): {
|
|
|
72
72
|
default: string;
|
|
73
73
|
description: string;
|
|
74
74
|
};
|
|
75
|
+
productId: {
|
|
76
|
+
required: boolean;
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
purchaseId: {
|
|
80
|
+
required: boolean;
|
|
81
|
+
description: string;
|
|
82
|
+
};
|
|
83
|
+
surveyId: {
|
|
84
|
+
required: boolean;
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
surveySlug: {
|
|
88
|
+
required: boolean;
|
|
89
|
+
description: string;
|
|
90
|
+
};
|
|
91
|
+
questionId: {
|
|
92
|
+
required: boolean;
|
|
93
|
+
description: string;
|
|
94
|
+
};
|
|
75
95
|
};
|
|
76
96
|
}[];
|
|
77
97
|
}> | NextResponse<{
|
|
@@ -80,7 +100,7 @@ declare function createAnalyticsCatalogHandler(deps: CatalogHandlerDeps): {
|
|
|
80
100
|
surface: string;
|
|
81
101
|
range: AnalyticsRange$1;
|
|
82
102
|
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[];
|
|
103
|
+
data: RevenueSummary | ConversionFunnel | CommerceLaneSummary | AttributionCoverage | TrafficOverview | YouTubeChannelOverview | TrafficRevenueCorrelation | TrafficDaily[] | RevenueDaily[] | YouTubeRevenueCorrelation | YouTubeDaily[] | SurveySummary | EmailRevenueOverview | SurveyRevenueCorrelation | ProductSurveyRevenueCorrelation | CheckoutAttributionReceipt | CheckoutSurveyFallbackReport | ValuePathSummary | RevenueByProduct[] | RevenueByCountry[] | RecentPurchase[] | AttributionCount[] | ShortlinkPerformance[] | RevenueBySource[] | ContentCorrelation[] | TopPage[] | TrafficSource[] | YouTubeVideoPerformance[] | YouTubeTrafficSource[] | SurveyListItem[] | SurveyResponsesDaily[] | SurveyQuestionBreakdown[] | SurveyResponseRow[];
|
|
84
104
|
meta: {
|
|
85
105
|
totalRows: number;
|
|
86
106
|
truncated: boolean;
|
package/dist/api/index.js
CHANGED
|
@@ -147,7 +147,7 @@ function createAnalyticsCatalogHandler(deps) {
|
|
|
147
147
|
return NextResponse.json({
|
|
148
148
|
ok: true,
|
|
149
149
|
endpoint: endpointPath,
|
|
150
|
-
description: `${appLabel} analytics
|
|
150
|
+
description: `${appLabel} analytics, revenue, attribution, traffic, YouTube, and content correlation`,
|
|
151
151
|
notes: [
|
|
152
152
|
"YouTube surfaces are useful for correlation/content analysis but lag by about 48 hours."
|
|
153
153
|
],
|
|
@@ -175,6 +175,26 @@ function createAnalyticsCatalogHandler(deps) {
|
|
|
175
175
|
limit: {
|
|
176
176
|
default: "20",
|
|
177
177
|
description: "Max rows for surfaces that support it (max 100)"
|
|
178
|
+
},
|
|
179
|
+
productId: {
|
|
180
|
+
required: false,
|
|
181
|
+
description: "Optional product filter for product-aware attribution surfaces"
|
|
182
|
+
},
|
|
183
|
+
purchaseId: {
|
|
184
|
+
required: false,
|
|
185
|
+
description: "Required for attribution/checkout-receipt"
|
|
186
|
+
},
|
|
187
|
+
surveyId: {
|
|
188
|
+
required: false,
|
|
189
|
+
description: "Optional survey ID filter for product survey correlation"
|
|
190
|
+
},
|
|
191
|
+
surveySlug: {
|
|
192
|
+
required: false,
|
|
193
|
+
description: "Optional survey slug filter for product survey correlation"
|
|
194
|
+
},
|
|
195
|
+
questionId: {
|
|
196
|
+
required: false,
|
|
197
|
+
description: "Optional question ID filter for product survey correlation"
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
200
|
}
|
|
@@ -206,6 +226,11 @@ function createAnalyticsCatalogHandler(deps) {
|
|
|
206
226
|
const surface = rawSurface;
|
|
207
227
|
const range = parseRange(searchParams.get("range"));
|
|
208
228
|
const limit = Math.min(Number(searchParams.get("limit") ?? 20), 100);
|
|
229
|
+
const productId = searchParams.get("productId") ?? void 0;
|
|
230
|
+
const purchaseId = searchParams.get("purchaseId") ?? void 0;
|
|
231
|
+
const surveyId = searchParams.get("surveyId") ?? void 0;
|
|
232
|
+
const surveySlug = searchParams.get("surveySlug") ?? void 0;
|
|
233
|
+
const questionId = searchParams.get("questionId") ?? void 0;
|
|
209
234
|
if (logger) {
|
|
210
235
|
logger.info("api.analytics.query", {
|
|
211
236
|
userId: access.user?.id ?? null,
|
|
@@ -213,12 +238,22 @@ function createAnalyticsCatalogHandler(deps) {
|
|
|
213
238
|
authMethod: access.authMethod ?? "unknown",
|
|
214
239
|
surface,
|
|
215
240
|
range,
|
|
216
|
-
limit
|
|
241
|
+
limit,
|
|
242
|
+
productId,
|
|
243
|
+
purchaseId,
|
|
244
|
+
surveyId,
|
|
245
|
+
surveySlug,
|
|
246
|
+
questionId
|
|
217
247
|
});
|
|
218
248
|
}
|
|
219
249
|
const result = await engine.query(surface, {
|
|
220
250
|
range,
|
|
221
|
-
limit
|
|
251
|
+
limit,
|
|
252
|
+
productId,
|
|
253
|
+
purchaseId,
|
|
254
|
+
surveyId,
|
|
255
|
+
surveySlug,
|
|
256
|
+
questionId
|
|
222
257
|
});
|
|
223
258
|
if (!result.ok) {
|
|
224
259
|
if (logger) {
|
|
@@ -274,8 +309,8 @@ __name(createAnalyticsCatalogHandler, "createAnalyticsCatalogHandler");
|
|
|
274
309
|
import { NextResponse as NextResponse2 } from "next/server";
|
|
275
310
|
function createTokenHandler(deps) {
|
|
276
311
|
const { db, deviceAccessToken, checkAccess, logger } = deps;
|
|
277
|
-
const ttlHours = deps.ttlHours ?? 24;
|
|
278
|
-
const ttlLabel = `${ttlHours} hours`;
|
|
312
|
+
const ttlHours = deps.ttlHours ?? 90 * 24;
|
|
313
|
+
const ttlLabel = ttlHours % 24 === 0 ? `${ttlHours / 24} days` : `${ttlHours} hours`;
|
|
279
314
|
const POST = /* @__PURE__ */ __name(async (request) => {
|
|
280
315
|
const access = await checkAccess(request);
|
|
281
316
|
if (!access.authorized || !access.userId) {
|
package/dist/api/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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\tproductId: {\n\t\t\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t'Optional product filter for product-aware attribution surfaces',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tpurchaseId: {\n\t\t\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\t\t\tdescription: 'Required for attribution/checkout-receipt',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tsurveyId: {\n\t\t\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t'Optional survey ID filter for product survey correlation',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tsurveySlug: {\n\t\t\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t'Optional survey slug filter for product survey correlation',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tquestionId: {\n\t\t\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t'Optional question ID filter for product survey correlation',\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\t\tconst productId = searchParams.get('productId') ?? undefined\n\t\tconst purchaseId = searchParams.get('purchaseId') ?? undefined\n\t\tconst surveyId = searchParams.get('surveyId') ?? undefined\n\t\tconst surveySlug = searchParams.get('surveySlug') ?? undefined\n\t\tconst questionId = searchParams.get('questionId') ?? undefined\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\tproductId,\n\t\t\t\tpurchaseId,\n\t\t\t\tsurveyId,\n\t\t\t\tsurveySlug,\n\t\t\t\tquestionId,\n\t\t\t})\n\t\t}\n\n\t\tconst result = await engine.query(surface, {\n\t\t\trange,\n\t\t\tlimit,\n\t\t\tproductId,\n\t\t\tpurchaseId,\n\t\t\tsurveyId,\n\t\t\tsurveySlug,\n\t\t\tquestionId,\n\t\t})\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 ?? 90 * 24\n\tconst ttlLabel =\n\t\tttlHours % 24 === 0 ? `${ttlHours / 24} days` : `${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","productId","purchaseId","surveyId","surveySlug","questionId","Math","min","Number","undefined","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"]}
|
package/dist/catalog.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { SurfaceMap, SurfaceName } from './types.js';
|
|
|
3
3
|
interface SurfaceEntry {
|
|
4
4
|
name: SurfaceName;
|
|
5
5
|
description: string;
|
|
6
|
-
category: 'revenue' | 'attribution' | 'traffic' | 'youtube' | 'correlation' | 'survey';
|
|
6
|
+
category: 'revenue' | 'attribution' | 'traffic' | 'youtube' | 'correlation' | 'survey' | 'value-path';
|
|
7
7
|
provider: 'database' | 'ga4' | 'youtube' | 'derived' | 'newsletter' | 'survey';
|
|
8
8
|
fn: string;
|
|
9
9
|
unavailableFix?: string;
|
package/dist/catalog.js
CHANGED
|
@@ -72,11 +72,18 @@ var catalog = {
|
|
|
72
72
|
},
|
|
73
73
|
"attribution/coverage": {
|
|
74
74
|
name: "attribution/coverage",
|
|
75
|
-
description: "Attributed vs dark revenue",
|
|
75
|
+
description: "Attributed vs dark paid revenue, with commerce lane context",
|
|
76
76
|
category: "attribution",
|
|
77
77
|
provider: "database",
|
|
78
78
|
fn: "getAttributedRevenueSummary"
|
|
79
79
|
},
|
|
80
|
+
"attribution/commerce-lanes": {
|
|
81
|
+
name: "attribution/commerce-lanes",
|
|
82
|
+
description: "Commerce record lanes: paid purchases, access grants, free upgrades, synthetic tests",
|
|
83
|
+
category: "attribution",
|
|
84
|
+
provider: "database",
|
|
85
|
+
fn: "getCommerceLaneSummary"
|
|
86
|
+
},
|
|
80
87
|
traffic: {
|
|
81
88
|
name: "traffic",
|
|
82
89
|
description: "GA4 traffic overview",
|
|
@@ -193,12 +200,47 @@ var catalog = {
|
|
|
193
200
|
provider: "newsletter",
|
|
194
201
|
fn: "getEmailCampaignAttribution"
|
|
195
202
|
},
|
|
203
|
+
"attribution/email-campaigns/strict": {
|
|
204
|
+
name: "attribution/email-campaigns/strict",
|
|
205
|
+
description: "Kit email broadcasts \u2192 shortlink clicks \u2192 strict purchase-field purchases and revenue per campaign",
|
|
206
|
+
category: "attribution",
|
|
207
|
+
provider: "newsletter",
|
|
208
|
+
fn: "getEmailCampaignAttributionStrict"
|
|
209
|
+
},
|
|
210
|
+
"attribution/checkout-receipt": {
|
|
211
|
+
name: "attribution/checkout-receipt",
|
|
212
|
+
description: "Read-only checkout attribution receipt for one purchase ID",
|
|
213
|
+
category: "attribution",
|
|
214
|
+
provider: "database",
|
|
215
|
+
fn: "getCheckoutAttributionReceipt"
|
|
216
|
+
},
|
|
217
|
+
"attribution/checkout-survey-fallback": {
|
|
218
|
+
name: "attribution/checkout-survey-fallback",
|
|
219
|
+
description: "Report-only dark purchase fallback from checkout survey responses",
|
|
220
|
+
category: "attribution",
|
|
221
|
+
provider: "database",
|
|
222
|
+
fn: "getCheckoutSurveyFallbackReport"
|
|
223
|
+
},
|
|
196
224
|
"correlation/survey-revenue": {
|
|
197
225
|
name: "correlation/survey-revenue",
|
|
198
226
|
description: "Survey respondents \u2192 purchase conversion by question/answer",
|
|
199
227
|
category: "correlation",
|
|
200
228
|
provider: "derived",
|
|
201
229
|
fn: "getSurveyRevenueCorrelation"
|
|
230
|
+
},
|
|
231
|
+
"correlation/survey-revenue/product": {
|
|
232
|
+
name: "correlation/survey-revenue/product",
|
|
233
|
+
description: "Product-filtered survey respondents \u2192 paid purchase conversion by question/answer",
|
|
234
|
+
category: "correlation",
|
|
235
|
+
provider: "derived",
|
|
236
|
+
fn: "getProductSurveyRevenueCorrelation"
|
|
237
|
+
},
|
|
238
|
+
"value-paths/summary": {
|
|
239
|
+
name: "value-paths/summary",
|
|
240
|
+
description: "Value Path progression, answer selection, drip fallback, and terminal completion",
|
|
241
|
+
category: "value-path",
|
|
242
|
+
provider: "database",
|
|
243
|
+
fn: "getValuePathSummary"
|
|
202
244
|
}
|
|
203
245
|
};
|
|
204
246
|
var ANALYTICS_CATALOG = catalog;
|
package/dist/catalog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["import type { SurfaceName } from './types'\n\nexport interface SurfaceEntry {\n\tname: SurfaceName\n\tdescription: string\n\tcategory:\n\t\t| 'revenue'\n\t\t| 'attribution'\n\t\t| 'traffic'\n\t\t| 'youtube'\n\t\t| 'correlation'\n\t\t| 'survey'\n\tprovider: 'database' | 'ga4' | 'youtube' | 'derived' | 'newsletter' | 'survey'\n\tfn: string\n\tunavailableFix?: string\n}\n\nexport const catalog: Record<SurfaceName, SurfaceEntry> = {\n\tsummary: {\n\t\tname: 'summary',\n\t\tdescription: 'Revenue overview: total, purchase count, AOV',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueSummary',\n\t},\n\t'revenue/daily': {\n\t\tname: 'revenue/daily',\n\t\tdescription: 'Revenue and purchase count per day',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueByDay',\n\t},\n\t'revenue/products': {\n\t\tname: 'revenue/products',\n\t\tdescription: 'Revenue grouped by product',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueByProduct',\n\t},\n\t'revenue/countries': {\n\t\tname: 'revenue/countries',\n\t\tdescription: 'Revenue grouped by country',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueByCountry',\n\t},\n\t'purchases/recent': {\n\t\tname: 'purchases/recent',\n\t\tdescription: 'Last N purchases',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRecentPurchases',\n\t},\n\tattribution: {\n\t\tname: 'attribution',\n\t\tdescription: 'Attribution event counts by type',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getAttributionSummary',\n\t},\n\t'attribution/shortlinks': {\n\t\tname: 'attribution/shortlinks',\n\t\tdescription: 'Per-shortlink click performance',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getShortlinkPerformance',\n\t},\n\t'attribution/sources': {\n\t\tname: 'attribution/sources',\n\t\tdescription: 'Revenue by first-touch source/medium/campaign',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueBySource',\n\t},\n\t'attribution/funnel': {\n\t\tname: 'attribution/funnel',\n\t\tdescription: 'Signup → purchase conversion funnel',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getConversionFunnel',\n\t},\n\t'attribution/content': {\n\t\tname: 'attribution/content',\n\t\tdescription: 'Content consumed by purchasers',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getContentPurchaseCorrelation',\n\t},\n\t'attribution/coverage': {\n\t\tname: 'attribution/coverage',\n\t\tdescription: 'Attributed vs dark revenue',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getAttributedRevenueSummary',\n\t},\n\ttraffic: {\n\t\tname: 'traffic',\n\t\tdescription: 'GA4 traffic overview',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getTrafficOverview',\n\t},\n\t'traffic/daily': {\n\t\tname: 'traffic/daily',\n\t\tdescription: 'GA4 daily sessions',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getSessionsByDay',\n\t},\n\t'traffic/pages': {\n\t\tname: 'traffic/pages',\n\t\tdescription: 'Top pages by pageviews',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getTopPages',\n\t},\n\t'traffic/sources': {\n\t\tname: 'traffic/sources',\n\t\tdescription: 'Traffic sources',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getTrafficSources',\n\t},\n\tyoutube: {\n\t\tname: 'youtube',\n\t\tdescription: 'Channel overview (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getChannelOverview',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'youtube/videos': {\n\t\tname: 'youtube/videos',\n\t\tdescription: 'Per-video performance (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getVideoPerformance',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'youtube/daily': {\n\t\tname: 'youtube/daily',\n\t\tdescription: 'Daily views + watch minutes (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getChannelTimeseries',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'youtube/sources': {\n\t\tname: 'youtube/sources',\n\t\tdescription: 'YouTube traffic sources (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getYouTubeTrafficSources',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'correlation/traffic-revenue': {\n\t\tname: 'correlation/traffic-revenue',\n\t\tdescription: 'GA4 sessions + revenue by day',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getTrafficRevenueCorrelation',\n\t},\n\t'correlation/youtube-revenue': {\n\t\tname: 'correlation/youtube-revenue',\n\t\tdescription: 'YouTube (≈48h lag) + GA4 + revenue overlay',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getYouTubeRevenueCorrelation',\n\t},\n\tsurveys: {\n\t\tname: 'surveys',\n\t\tdescription: 'Survey overview: total surveys, responses, respondents',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveySummary',\n\t},\n\t'surveys/list': {\n\t\tname: 'surveys/list',\n\t\tdescription: 'All surveys with response counts',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyList',\n\t},\n\t'surveys/daily': {\n\t\tname: 'surveys/daily',\n\t\tdescription: 'Daily survey response volume',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyResponsesByDay',\n\t},\n\t'surveys/questions': {\n\t\tname: 'surveys/questions',\n\t\tdescription: 'Top questions by response count with answer distribution',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyQuestionBreakdown',\n\t},\n\t'surveys/responses': {\n\t\tname: 'surveys/responses',\n\t\tdescription:\n\t\t\t'Individual survey responses as flat rows (multi-choice and open-ended)',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyResponses',\n\t},\n\t'attribution/email-campaigns': {\n\t\tname: 'attribution/email-campaigns',\n\t\tdescription:\n\t\t\t'Kit email broadcasts → shortlink clicks → signups → purchases → revenue per campaign',\n\t\tcategory: 'attribution',\n\t\tprovider: 'newsletter',\n\t\tfn: 'getEmailCampaignAttribution',\n\t},\n\t'correlation/survey-revenue': {\n\t\tname: 'correlation/survey-revenue',\n\t\tdescription: 'Survey respondents → purchase conversion by question/answer',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getSurveyRevenueCorrelation',\n\t},\n} as const satisfies Record<SurfaceName, SurfaceEntry>\n\nexport const ANALYTICS_CATALOG = catalog\n"],"mappings":"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","names":["catalog","summary","name","description","category","provider","fn","attribution","traffic","youtube","unavailableFix","surveys","ANALYTICS_CATALOG"]}
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["import type { SurfaceName } from './types'\n\nexport interface SurfaceEntry {\n\tname: SurfaceName\n\tdescription: string\n\tcategory:\n\t\t| 'revenue'\n\t\t| 'attribution'\n\t\t| 'traffic'\n\t\t| 'youtube'\n\t\t| 'correlation'\n\t\t| 'survey'\n\t\t| 'value-path'\n\tprovider: 'database' | 'ga4' | 'youtube' | 'derived' | 'newsletter' | 'survey'\n\tfn: string\n\tunavailableFix?: string\n}\n\nexport const catalog: Record<SurfaceName, SurfaceEntry> = {\n\tsummary: {\n\t\tname: 'summary',\n\t\tdescription: 'Revenue overview: total, purchase count, AOV',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueSummary',\n\t},\n\t'revenue/daily': {\n\t\tname: 'revenue/daily',\n\t\tdescription: 'Revenue and purchase count per day',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueByDay',\n\t},\n\t'revenue/products': {\n\t\tname: 'revenue/products',\n\t\tdescription: 'Revenue grouped by product',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueByProduct',\n\t},\n\t'revenue/countries': {\n\t\tname: 'revenue/countries',\n\t\tdescription: 'Revenue grouped by country',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueByCountry',\n\t},\n\t'purchases/recent': {\n\t\tname: 'purchases/recent',\n\t\tdescription: 'Last N purchases',\n\t\tcategory: 'revenue',\n\t\tprovider: 'database',\n\t\tfn: 'getRecentPurchases',\n\t},\n\tattribution: {\n\t\tname: 'attribution',\n\t\tdescription: 'Attribution event counts by type',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getAttributionSummary',\n\t},\n\t'attribution/shortlinks': {\n\t\tname: 'attribution/shortlinks',\n\t\tdescription: 'Per-shortlink click performance',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getShortlinkPerformance',\n\t},\n\t'attribution/sources': {\n\t\tname: 'attribution/sources',\n\t\tdescription: 'Revenue by first-touch source/medium/campaign',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getRevenueBySource',\n\t},\n\t'attribution/funnel': {\n\t\tname: 'attribution/funnel',\n\t\tdescription: 'Signup → purchase conversion funnel',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getConversionFunnel',\n\t},\n\t'attribution/content': {\n\t\tname: 'attribution/content',\n\t\tdescription: 'Content consumed by purchasers',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getContentPurchaseCorrelation',\n\t},\n\t'attribution/coverage': {\n\t\tname: 'attribution/coverage',\n\t\tdescription: 'Attributed vs dark paid revenue, with commerce lane context',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getAttributedRevenueSummary',\n\t},\n\t'attribution/commerce-lanes': {\n\t\tname: 'attribution/commerce-lanes',\n\t\tdescription:\n\t\t\t'Commerce record lanes: paid purchases, access grants, free upgrades, synthetic tests',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getCommerceLaneSummary',\n\t},\n\ttraffic: {\n\t\tname: 'traffic',\n\t\tdescription: 'GA4 traffic overview',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getTrafficOverview',\n\t},\n\t'traffic/daily': {\n\t\tname: 'traffic/daily',\n\t\tdescription: 'GA4 daily sessions',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getSessionsByDay',\n\t},\n\t'traffic/pages': {\n\t\tname: 'traffic/pages',\n\t\tdescription: 'Top pages by pageviews',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getTopPages',\n\t},\n\t'traffic/sources': {\n\t\tname: 'traffic/sources',\n\t\tdescription: 'Traffic sources',\n\t\tcategory: 'traffic',\n\t\tprovider: 'ga4',\n\t\tfn: 'getTrafficSources',\n\t},\n\tyoutube: {\n\t\tname: 'youtube',\n\t\tdescription: 'Channel overview (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getChannelOverview',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'youtube/videos': {\n\t\tname: 'youtube/videos',\n\t\tdescription: 'Per-video performance (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getVideoPerformance',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'youtube/daily': {\n\t\tname: 'youtube/daily',\n\t\tdescription: 'Daily views + watch minutes (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getChannelTimeseries',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'youtube/sources': {\n\t\tname: 'youtube/sources',\n\t\tdescription: 'YouTube traffic sources (≈48h lag)',\n\t\tcategory: 'youtube',\n\t\tprovider: 'youtube',\n\t\tfn: 'getYouTubeTrafficSources',\n\t\tunavailableFix: 'Complete OAuth at /api/analytics/youtube-auth',\n\t},\n\t'correlation/traffic-revenue': {\n\t\tname: 'correlation/traffic-revenue',\n\t\tdescription: 'GA4 sessions + revenue by day',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getTrafficRevenueCorrelation',\n\t},\n\t'correlation/youtube-revenue': {\n\t\tname: 'correlation/youtube-revenue',\n\t\tdescription: 'YouTube (≈48h lag) + GA4 + revenue overlay',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getYouTubeRevenueCorrelation',\n\t},\n\tsurveys: {\n\t\tname: 'surveys',\n\t\tdescription: 'Survey overview: total surveys, responses, respondents',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveySummary',\n\t},\n\t'surveys/list': {\n\t\tname: 'surveys/list',\n\t\tdescription: 'All surveys with response counts',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyList',\n\t},\n\t'surveys/daily': {\n\t\tname: 'surveys/daily',\n\t\tdescription: 'Daily survey response volume',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyResponsesByDay',\n\t},\n\t'surveys/questions': {\n\t\tname: 'surveys/questions',\n\t\tdescription: 'Top questions by response count with answer distribution',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyQuestionBreakdown',\n\t},\n\t'surveys/responses': {\n\t\tname: 'surveys/responses',\n\t\tdescription:\n\t\t\t'Individual survey responses as flat rows (multi-choice and open-ended)',\n\t\tcategory: 'survey',\n\t\tprovider: 'survey',\n\t\tfn: 'getSurveyResponses',\n\t},\n\t'attribution/email-campaigns': {\n\t\tname: 'attribution/email-campaigns',\n\t\tdescription:\n\t\t\t'Kit email broadcasts → shortlink clicks → signups → purchases → revenue per campaign',\n\t\tcategory: 'attribution',\n\t\tprovider: 'newsletter',\n\t\tfn: 'getEmailCampaignAttribution',\n\t},\n\t'attribution/email-campaigns/strict': {\n\t\tname: 'attribution/email-campaigns/strict',\n\t\tdescription:\n\t\t\t'Kit email broadcasts → shortlink clicks → strict purchase-field purchases and revenue per campaign',\n\t\tcategory: 'attribution',\n\t\tprovider: 'newsletter',\n\t\tfn: 'getEmailCampaignAttributionStrict',\n\t},\n\t'attribution/checkout-receipt': {\n\t\tname: 'attribution/checkout-receipt',\n\t\tdescription: 'Read-only checkout attribution receipt for one purchase ID',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getCheckoutAttributionReceipt',\n\t},\n\t'attribution/checkout-survey-fallback': {\n\t\tname: 'attribution/checkout-survey-fallback',\n\t\tdescription:\n\t\t\t'Report-only dark purchase fallback from checkout survey responses',\n\t\tcategory: 'attribution',\n\t\tprovider: 'database',\n\t\tfn: 'getCheckoutSurveyFallbackReport',\n\t},\n\t'correlation/survey-revenue': {\n\t\tname: 'correlation/survey-revenue',\n\t\tdescription: 'Survey respondents → purchase conversion by question/answer',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getSurveyRevenueCorrelation',\n\t},\n\t'correlation/survey-revenue/product': {\n\t\tname: 'correlation/survey-revenue/product',\n\t\tdescription:\n\t\t\t'Product-filtered survey respondents → paid purchase conversion by question/answer',\n\t\tcategory: 'correlation',\n\t\tprovider: 'derived',\n\t\tfn: 'getProductSurveyRevenueCorrelation',\n\t},\n\t'value-paths/summary': {\n\t\tname: 'value-paths/summary',\n\t\tdescription:\n\t\t\t'Value Path progression, answer selection, drip fallback, and terminal completion',\n\t\tcategory: 'value-path',\n\t\tprovider: 'database',\n\t\tfn: 'getValuePathSummary',\n\t},\n} as const satisfies Record<SurfaceName, SurfaceEntry>\n\nexport const ANALYTICS_CATALOG = catalog\n"],"mappings":"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","names":["catalog","summary","name","description","category","provider","fn","attribution","traffic","youtube","unavailableFix","surveys","ANALYTICS_CATALOG"]}
|
|
@@ -107,6 +107,35 @@ interface DashboardData {
|
|
|
107
107
|
count: number;
|
|
108
108
|
}[];
|
|
109
109
|
}[] | null;
|
|
110
|
+
valuePaths?: {
|
|
111
|
+
contacts: number;
|
|
112
|
+
events: number;
|
|
113
|
+
intents: number;
|
|
114
|
+
completedIntents: number;
|
|
115
|
+
pendingIntents: number;
|
|
116
|
+
blockedIntents: number;
|
|
117
|
+
answerEvents: number;
|
|
118
|
+
dripEvents: number;
|
|
119
|
+
enteredEvents: number;
|
|
120
|
+
participantsWithAnswerClicks: number;
|
|
121
|
+
participantsWithNoAnswerClicks: number;
|
|
122
|
+
terminalParticipants: number;
|
|
123
|
+
answerOptions: {
|
|
124
|
+
key: string;
|
|
125
|
+
step: string;
|
|
126
|
+
optionValue: string;
|
|
127
|
+
count: number;
|
|
128
|
+
}[];
|
|
129
|
+
answerSteps: {
|
|
130
|
+
step: string;
|
|
131
|
+
count: number;
|
|
132
|
+
}[];
|
|
133
|
+
terminalSteps: {
|
|
134
|
+
emailResourceId: string;
|
|
135
|
+
count: number;
|
|
136
|
+
}[];
|
|
137
|
+
notes: string[];
|
|
138
|
+
} | null;
|
|
110
139
|
surveyCorrelation: {
|
|
111
140
|
totalRespondents: number;
|
|
112
141
|
respondentsWhoPurchased: number;
|
package/dist/components/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/components/omnibus-dashboard.tsx
|
|
5
5
|
import React3, { useState as useState2, useTransition } from "react";
|
|
6
|
-
import { CheckIcon, ChevronDownIcon, ClipboardIcon, ClipboardListIcon, ClockIcon, DollarSignIcon, ExternalLinkIcon, FilmIcon, GlobeIcon, LinkIcon, Loader2Icon, MousePointerClickIcon, PlayIcon, ShoppingCartIcon, TrendingUpIcon } from "lucide-react";
|
|
6
|
+
import { CheckIcon, ChevronDownIcon, ClipboardIcon, ClipboardListIcon, ClockIcon, DollarSignIcon, ExternalLinkIcon, FilmIcon, GlobeIcon, LinkIcon, Loader2Icon, MousePointerClickIcon, PlayIcon, ShoppingCartIcon, TrendingUpIcon, RouteIcon } from "lucide-react";
|
|
7
7
|
import { parseAsStringLiteral, useQueryState } from "nuqs";
|
|
8
8
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@coursebuilder/ui";
|
|
9
9
|
|
|
@@ -731,6 +731,7 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
|
|
|
731
731
|
const hasTraffic = data.traffic != null;
|
|
732
732
|
const hasSurveys = data.surveySegments != null && data.surveySegments.length > 0;
|
|
733
733
|
const hasSurveyCorrelation = data.surveyCorrelation != null;
|
|
734
|
+
const hasValuePaths = data.valuePaths != null && data.valuePaths.contacts > 0;
|
|
734
735
|
const hasShortlinks = data.shortlinks.length > 0;
|
|
735
736
|
return /* @__PURE__ */ React3.createElement("div", {
|
|
736
737
|
className: "flex flex-col gap-5 lg:gap-7"
|
|
@@ -787,6 +788,11 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
|
|
|
787
788
|
value: fmtK(totalClicks),
|
|
788
789
|
sub: `${data.shortlinks.length} active \xB7 ${signupCount} signups`,
|
|
789
790
|
icon: MousePointerClickIcon
|
|
791
|
+
}), hasValuePaths && /* @__PURE__ */ React3.createElement(Stat, {
|
|
792
|
+
label: "Value Paths",
|
|
793
|
+
value: `${data.valuePaths.terminalParticipants}/${data.valuePaths.contacts}`,
|
|
794
|
+
sub: `${data.valuePaths.answerEvents} answers \xB7 ${data.valuePaths.dripEvents} drips`,
|
|
795
|
+
icon: RouteIcon
|
|
790
796
|
}), hasSurveys && /* @__PURE__ */ React3.createElement(Stat, {
|
|
791
797
|
label: "Surveys",
|
|
792
798
|
value: `${data.surveySegments.length}`,
|
|
@@ -866,7 +872,90 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
|
|
|
866
872
|
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
867
873
|
}, "Total Purchases"), /* @__PURE__ */ React3.createElement("span", {
|
|
868
874
|
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
869
|
-
}, data.attributionCoverage.totalPurchases.toLocaleString()))))),
|
|
875
|
+
}, data.attributionCoverage.totalPurchases.toLocaleString()))))), hasValuePaths && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
876
|
+
className: "pb-3"
|
|
877
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
878
|
+
className: "flex items-center gap-2.5"
|
|
879
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
880
|
+
className: "flex h-7 w-7 items-center justify-center rounded-lg bg-sky-500/10 text-sky-500"
|
|
881
|
+
}, /* @__PURE__ */ React3.createElement(RouteIcon, {
|
|
882
|
+
className: "h-3.5 w-3.5"
|
|
883
|
+
})), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
884
|
+
className: "text-sm font-semibold"
|
|
885
|
+
}, "Value Paths"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
886
|
+
className: "text-[11px]"
|
|
887
|
+
}, "Progression, answer clicks, drip fallback, and terminal completion")))), /* @__PURE__ */ React3.createElement(CardContent, {
|
|
888
|
+
className: "space-y-5"
|
|
889
|
+
}, (() => {
|
|
890
|
+
const vp = data.valuePaths;
|
|
891
|
+
const completionRate = vp.contacts > 0 ? vp.terminalParticipants / vp.contacts * 100 : 0;
|
|
892
|
+
const clickRate = vp.contacts > 0 ? vp.participantsWithAnswerClicks / vp.contacts * 100 : 0;
|
|
893
|
+
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", {
|
|
894
|
+
className: "grid grid-cols-2 gap-2 sm:grid-cols-5"
|
|
895
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
896
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
897
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
898
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
899
|
+
}, "Contacts"), /* @__PURE__ */ React3.createElement("span", {
|
|
900
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
901
|
+
}, vp.contacts.toLocaleString())), /* @__PURE__ */ React3.createElement("div", {
|
|
902
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
903
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
904
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
905
|
+
}, "Terminal"), /* @__PURE__ */ React3.createElement("span", {
|
|
906
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
907
|
+
}, vp.terminalParticipants.toLocaleString()), /* @__PURE__ */ React3.createElement("span", {
|
|
908
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
909
|
+
}, completionRate.toFixed(0), "% completed")), /* @__PURE__ */ React3.createElement("div", {
|
|
910
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
911
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
912
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
913
|
+
}, "Answered"), /* @__PURE__ */ React3.createElement("span", {
|
|
914
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
915
|
+
}, vp.participantsWithAnswerClicks.toLocaleString()), /* @__PURE__ */ React3.createElement("span", {
|
|
916
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
917
|
+
}, clickRate.toFixed(0), "% clicked")), /* @__PURE__ */ React3.createElement("div", {
|
|
918
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
919
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
920
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
921
|
+
}, "Drips"), /* @__PURE__ */ React3.createElement("span", {
|
|
922
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
923
|
+
}, vp.dripEvents.toLocaleString())), /* @__PURE__ */ React3.createElement("div", {
|
|
924
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
925
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
926
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
927
|
+
}, "Blocked"), /* @__PURE__ */ React3.createElement("span", {
|
|
928
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
929
|
+
}, vp.blockedIntents.toLocaleString()))), vp.answerOptions.length > 0 && /* @__PURE__ */ React3.createElement("div", {
|
|
930
|
+
className: "space-y-2"
|
|
931
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
932
|
+
className: "text-muted-foreground text-[11px]"
|
|
933
|
+
}, "Top answer choices by captured answer event"), /* @__PURE__ */ React3.createElement("div", {
|
|
934
|
+
className: "space-y-1.5"
|
|
935
|
+
}, vp.answerOptions.slice(0, 8).map((answer) => {
|
|
936
|
+
const pct = vp.answerEvents > 0 ? answer.count / vp.answerEvents * 100 : 0;
|
|
937
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
938
|
+
key: answer.key,
|
|
939
|
+
className: "flex items-center gap-2.5"
|
|
940
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
941
|
+
className: "text-muted-foreground w-[140px] shrink-0 truncate text-right text-[12px]",
|
|
942
|
+
title: answer.key
|
|
943
|
+
}, answer.optionValue), /* @__PURE__ */ React3.createElement("div", {
|
|
944
|
+
className: "bg-muted/50 relative h-5 flex-1 overflow-hidden rounded"
|
|
945
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
946
|
+
className: "absolute inset-y-0 left-0 rounded bg-sky-500/20",
|
|
947
|
+
style: {
|
|
948
|
+
width: `${Math.max(pct, 1)}%`
|
|
949
|
+
}
|
|
950
|
+
}), /* @__PURE__ */ React3.createElement("div", {
|
|
951
|
+
className: "relative flex h-full items-center px-2"
|
|
952
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
953
|
+
className: "text-foreground/70 text-[11px] font-medium tabular-nums"
|
|
954
|
+
}, pct.toFixed(0), "%"))), /* @__PURE__ */ React3.createElement("span", {
|
|
955
|
+
className: "text-muted-foreground w-10 shrink-0 text-right text-[11px] tabular-nums"
|
|
956
|
+
}, answer.count.toLocaleString()));
|
|
957
|
+
}))));
|
|
958
|
+
})())), hasSurveys && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
870
959
|
className: "pb-3"
|
|
871
960
|
}, /* @__PURE__ */ React3.createElement("div", {
|
|
872
961
|
className: "flex items-center justify-between"
|