@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.
@@ -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 \u2014 revenue, attribution, traffic, YouTube, and content correlation`,
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) {
@@ -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;
@@ -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;
@@ -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()))))), hasSurveys && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
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"