@coursebuilder/analytics 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/api/index.d.ts +158 -0
- package/dist/api/index.js +317 -0
- package/dist/api/index.js.map +1 -0
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.js +209 -0
- package/dist/catalog.js.map +1 -0
- package/dist/components/index.d.ts +172 -0
- package/dist/components/index.js +1258 -0
- package/dist/components/index.js.map +1 -0
- package/dist/engine.d.ts +20 -0
- package/dist/engine.js +350 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/database.d.ts +79 -0
- package/dist/providers/database.js +533 -0
- package/dist/providers/database.js.map +1 -0
- package/dist/providers/derived.d.ts +45 -0
- package/dist/providers/derived.js +32 -0
- package/dist/providers/derived.js.map +1 -0
- package/dist/providers/ga4.d.ts +43 -0
- package/dist/providers/ga4.js +220 -0
- package/dist/providers/ga4.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +1239 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mux.d.ts +103 -0
- package/dist/providers/mux.js +241 -0
- package/dist/providers/mux.js.map +1 -0
- package/dist/providers/survey.d.ts +102 -0
- package/dist/providers/survey.js +233 -0
- package/dist/providers/survey.js.map +1 -0
- package/dist/types.d.ts +303 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +101 -0
- package/src/api/catalog-handler.ts +321 -0
- package/src/api/index.ts +4 -0
- package/src/api/token-handler.ts +71 -0
- package/src/catalog.ts +223 -0
- package/src/components/country-chart.tsx +114 -0
- package/src/components/index.ts +5 -0
- package/src/components/omnibus-dashboard.tsx +1460 -0
- package/src/components/revenue-chart.tsx +251 -0
- package/src/components/use-chart-colors.ts +75 -0
- package/src/engine.ts +201 -0
- package/src/index.ts +7 -0
- package/src/providers/database.ts +795 -0
- package/src/providers/derived.ts +79 -0
- package/src/providers/ga4.ts +173 -0
- package/src/providers/index.ts +44 -0
- package/src/providers/mux.ts +438 -0
- package/src/providers/survey.ts +487 -0
- package/src/types.ts +333 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/database.ts","../../src/providers/ga4.ts","../../src/providers/derived.ts","../../src/providers/mux.ts","../../src/providers/survey.ts"],"sourcesContent":["import {\n\tand,\n\tcount,\n\tdesc,\n\teq,\n\tgt,\n\tgte,\n\tinArray,\n\tlte,\n\tsql,\n\tsum,\n} from 'drizzle-orm'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AnalyticsTimeRange = '24h' | '7d' | '30d' | '90d' | 'all'\n\nexport interface AttributionTrailEvent {\n\ttype: 'click' | 'signup' | 'progress' | 'purchase'\n\ttimestamp: Date\n\tdetail: Record<string, any>\n}\n\nexport interface AttributionTrail {\n\tuser: {\n\t\tid: string\n\t\temail: string | null\n\t\tname: string | null\n\t\tcreatedAt: Date\n\t} | null\n\tevents: AttributionTrailEvent[]\n\tpurchases: {\n\t\tid: string\n\t\ttotalAmount: number\n\t\tproductName: string\n\t\tcreatedAt: Date\n\t\tcountry: string | null\n\t\tutmSource: string | null\n\t\tutmMedium: string | null\n\t\tutmCampaign: string | null\n\t}[]\n}\n\n// ─── Schema type ─────────────────────────────────────────────────────────────\n\nexport interface DatabaseAnalyticsSchema {\n\tpurchases: any\n\tproducts: any\n\tusers: any\n\tcoupon: any\n\tresourceProgress: any\n\tshortlink: any\n\tshortlinkAttribution: any\n\tshortlinkClick: any\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a database analytics provider that wraps all analytics query\n * functions with an injected drizzle db instance and schema tables.\n *\n * @param db - Drizzle database instance\n * @param schema - Object containing the required table references\n */\nexport function createDatabaseProvider(\n\tdb: any,\n\tschema: DatabaseAnalyticsSchema,\n) {\n\tconst {\n\t\tpurchases,\n\t\tproducts,\n\t\tusers,\n\t\tcoupon,\n\t\tresourceProgress,\n\t\tshortlink,\n\t\tshortlinkAttribution,\n\t\tshortlinkClick,\n\t} = schema\n\n\t// ─── Internal helpers ───────────────────────────────────────────────────\n\n\tconst PAID_STATUSES = ['Valid', 'Restricted'] as const\n\n\tfunction paidPurchase() {\n\t\treturn inArray(purchases.status, [...PAID_STATUSES])\n\t}\n\n\tfunction rangeToDate(range: AnalyticsTimeRange): Date | null {\n\t\tif (range === 'all') return null\n\t\tconst now = new Date()\n\t\tconst hours: Record<string, number> = {\n\t\t\t'24h': 24,\n\t\t\t'7d': 7 * 24,\n\t\t\t'30d': 30 * 24,\n\t\t\t'90d': 90 * 24,\n\t\t}\n\t\treturn new Date(now.getTime() - (hours[range] ?? 30 * 24) * 60 * 60 * 1000)\n\t}\n\n\t// ─── Revenue ───────────────────────────────────────────────────────────\n\n\tasync function getRevenueSummary(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst [totals] = await db\n\t\t\t.select({\n\t\t\t\ttotalRevenue: sum(purchases.totalAmount),\n\t\t\t\tpurchaseCount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\n\t\treturn {\n\t\t\ttotalRevenue: Number(totals?.totalRevenue ?? 0),\n\t\t\tpurchaseCount: totals?.purchaseCount ?? 0,\n\t\t\tavgOrderValue:\n\t\t\t\ttotals?.purchaseCount && totals.purchaseCount > 0\n\t\t\t\t\t? Number(totals.totalRevenue ?? 0) / totals.purchaseCount\n\t\t\t\t\t: 0,\n\t\t}\n\t}\n\n\tasync function getRevenueByDay(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tdate: sql<string>`DATE(${purchases.createdAt})`.as('date'),\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(sql`DATE(${purchases.createdAt})`)\n\t\t\t.orderBy(sql`DATE(${purchases.createdAt})`)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tdate: r.date,\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\t/**\n\t * Revenue by day for the previous period of equal length.\n\t * E.g., if range = '30d', returns the 30 days before those 30 days.\n\t * Returns data with a `dayOffset` (0 = start of period) for overlay\n\t * alignment.\n\t */\n\tasync function getPreviousPeriodRevenueByDay(\n\t\trange: AnalyticsTimeRange = '30d',\n\t) {\n\t\tif (range === 'all') return []\n\n\t\tconst hours: Record<string, number> = {\n\t\t\t'24h': 24,\n\t\t\t'7d': 7 * 24,\n\t\t\t'30d': 30 * 24,\n\t\t\t'90d': 90 * 24,\n\t\t}\n\t\tconst periodMs = (hours[range] ?? 30 * 24) * 60 * 60 * 1000\n\t\tconst now = new Date()\n\t\tconst periodStart = new Date(now.getTime() - periodMs)\n\t\tconst prevStart = new Date(periodStart.getTime() - periodMs)\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tdate: sql<string>`DATE(${purchases.createdAt})`.as('date'),\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\tpaidPurchase(),\n\t\t\t\t\tgte(purchases.createdAt, prevStart),\n\t\t\t\t\tlte(purchases.createdAt, periodStart),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.groupBy(sql`DATE(${purchases.createdAt})`)\n\t\t\t.orderBy(sql`DATE(${purchases.createdAt})`)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tdate: r.date,\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getRevenueByProduct(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tproductId: purchases.productId,\n\t\t\t\tproductName: products.name,\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.leftJoin(products, eq(purchases.productId, products.id))\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(purchases.productId, products.name)\n\t\t\t.orderBy(desc(sum(purchases.totalAmount)))\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tproductId: r.productId,\n\t\t\tproductName: r.productName ?? '(unknown)',\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getRevenueByCountry(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tcountry: purchases.country,\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(purchases.country)\n\t\t\t.orderBy(desc(sum(purchases.totalAmount)))\n\t\t\t.limit(20)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tcountry: r.country ?? '(unknown)',\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getRecentPurchases(\n\t\tlimit: number = 20,\n\t\tfilter: 'all' | 'team' | 'individual' = 'all',\n\t\trange: AnalyticsTimeRange = 'all',\n\t) {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tif (filter === 'team') {\n\t\t\t// Multi-seat purchases: join coupon to filter seats > 1, sort by amount\n\t\t\tconditions.push(sql`${purchases.bulkCouponId} IS NOT NULL`)\n\n\t\t\tconst rows = await db\n\t\t\t\t.select({\n\t\t\t\t\tid: purchases.id,\n\t\t\t\t\tcreatedAt: purchases.createdAt,\n\t\t\t\t\ttotalAmount: purchases.totalAmount,\n\t\t\t\t\tproductName: products.name,\n\t\t\t\t\tproductId: purchases.productId,\n\t\t\t\t\tcountry: purchases.country,\n\t\t\t\t\tcouponId: purchases.couponId,\n\t\t\t\t\tuserId: purchases.userId,\n\t\t\t\t\tuserName: users.name,\n\t\t\t\t\tuserEmail: users.email,\n\t\t\t\t\torganizationId: purchases.organizationId,\n\t\t\t\t\tseats: coupon.maxUses,\n\t\t\t\t})\n\t\t\t\t.from(purchases)\n\t\t\t\t.leftJoin(products, eq(purchases.productId, products.id))\n\t\t\t\t.leftJoin(users, eq(purchases.userId, users.id))\n\t\t\t\t.leftJoin(coupon, eq(purchases.bulkCouponId, coupon.id))\n\t\t\t\t.where(and(...conditions, gt(coupon.maxUses, 1)))\n\t\t\t\t.orderBy(desc(purchases.totalAmount))\n\t\t\t\t.limit(limit)\n\n\t\t\treturn rows.map((r: any) => ({\n\t\t\t\tid: r.id,\n\t\t\t\tcreatedAt: r.createdAt,\n\t\t\t\ttotalAmount: Number(r.totalAmount),\n\t\t\t\tproductName: r.productName ?? '(unknown)',\n\t\t\t\tproductId: r.productId,\n\t\t\t\tcountry: r.country,\n\t\t\t\tcouponId: r.couponId,\n\t\t\t\tuserName: r.userName ?? null,\n\t\t\t\tuserEmail: r.userEmail ?? null,\n\t\t\t\tisTeam: true,\n\t\t\t\tseats: r.seats ?? null,\n\t\t\t}))\n\t\t}\n\n\t\tif (filter === 'individual') {\n\t\t\tconditions.push(sql`${purchases.bulkCouponId} IS NULL`)\n\t\t}\n\n\t\tconst rows = await db.query.purchases.findMany({\n\t\t\twhere: and(...conditions),\n\t\t\torderBy: [desc(purchases.totalAmount)],\n\t\t\tlimit,\n\t\t\twith: {\n\t\t\t\tproduct: true,\n\t\t\t\tuser: true,\n\t\t\t},\n\t\t})\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tid: r.id,\n\t\t\tcreatedAt: r.createdAt,\n\t\t\ttotalAmount: Number(r.totalAmount),\n\t\t\tproductName: r.product?.name ?? '(unknown)',\n\t\t\tproductId: r.productId,\n\t\t\tcountry: r.country,\n\t\t\tcouponId: r.couponId,\n\t\t\tuserName: r.user?.name ?? null,\n\t\t\tuserEmail: r.user?.email ?? null,\n\t\t\tisTeam: r.organizationId != null,\n\t\t\tseats: null as number | null,\n\t\t}))\n\t}\n\n\t// ─── Attribution ────────────────────────────────────────────────────────\n\n\tasync function getAttributionSummary(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions: any[] = []\n\t\tif (since) conditions.push(gte(shortlinkAttribution.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\ttype: shortlinkAttribution.type,\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(shortlinkAttribution)\n\t\t\t.where(conditions.length > 0 ? and(...conditions) : undefined)\n\t\t\t.groupBy(shortlinkAttribution.type)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\ttype: r.type,\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getShortlinkPerformance(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst clickConditions: any[] = []\n\t\tif (since) clickConditions.push(gte(shortlinkClick.timestamp, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tshortlinkId: shortlinkClick.shortlinkId,\n\t\t\t\tslug: shortlink.slug,\n\t\t\t\turl: shortlink.url,\n\t\t\t\tclicks: count(),\n\t\t\t})\n\t\t\t.from(shortlinkClick)\n\t\t\t.innerJoin(shortlink, eq(shortlinkClick.shortlinkId, shortlink.id))\n\t\t\t.where(clickConditions.length > 0 ? and(...clickConditions) : undefined)\n\t\t\t.groupBy(shortlinkClick.shortlinkId, shortlink.slug, shortlink.url)\n\t\t\t.orderBy(desc(count()))\n\t\t\t.limit(20)\n\n\t\t// Get attribution counts per shortlink\n\t\tconst attrConditions: any[] = []\n\t\tif (since) attrConditions.push(gte(shortlinkAttribution.createdAt, since))\n\n\t\tconst attrRows = await db\n\t\t\t.select({\n\t\t\t\tshortlinkId: shortlinkAttribution.shortlinkId,\n\t\t\t\ttype: shortlinkAttribution.type,\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(shortlinkAttribution)\n\t\t\t.where(attrConditions.length > 0 ? and(...attrConditions) : undefined)\n\t\t\t.groupBy(shortlinkAttribution.shortlinkId, shortlinkAttribution.type)\n\n\t\tconst attrMap = new Map<string, { signups: number; purchases: number }>()\n\t\tfor (const a of attrRows) {\n\t\t\tconst existing = attrMap.get(a.shortlinkId) ?? {\n\t\t\t\tsignups: 0,\n\t\t\t\tpurchases: 0,\n\t\t\t}\n\t\t\tif (a.type === 'signup') existing.signups = a.count\n\t\t\tif (a.type === 'purchase') existing.purchases = a.count\n\t\t\tattrMap.set(a.shortlinkId, existing)\n\t\t}\n\n\t\treturn rows.map((r: any) => {\n\t\t\tconst attr = attrMap.get(r.shortlinkId)\n\t\t\treturn {\n\t\t\t\tshortlinkId: r.shortlinkId,\n\t\t\t\tslug: r.slug,\n\t\t\t\turl: r.url,\n\t\t\t\tclicks: r.clicks,\n\t\t\t\tsignups: attr?.signups ?? 0,\n\t\t\t\tpurchases: attr?.purchases ?? 0,\n\t\t\t}\n\t\t})\n\t}\n\n\t// ─── Revenue Attribution ─────────────────────────────────────────────────\n\n\tasync function getRevenueBySource(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tsource:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`.as(\n\t\t\t\t\t\t'source',\n\t\t\t\t\t),\n\t\t\t\tmedium:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`.as(\n\t\t\t\t\t\t'medium',\n\t\t\t\t\t),\n\t\t\t\tcampaign:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`.as(\n\t\t\t\t\t\t'campaign',\n\t\t\t\t\t),\n\t\t\t\trevenue: sum(purchases.totalAmount),\n\t\t\t\tcount: count(),\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\t\t\t.groupBy(\n\t\t\t\tsql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`,\n\t\t\t\tsql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`,\n\t\t\t\tsql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`,\n\t\t\t)\n\t\t\t.orderBy(desc(sum(purchases.totalAmount)))\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tsource: r.source ?? null,\n\t\t\tmedium: r.medium ?? null,\n\t\t\tcampaign: r.campaign ?? null,\n\t\t\trevenue: Number(r.revenue ?? 0),\n\t\t\tcount: r.count,\n\t\t}))\n\t}\n\n\tasync function getConversionFunnel(range: AnalyticsTimeRange = '30d') {\n\t\tconst since = rangeToDate(range)\n\n\t\tconst userConditions = since ? [gte(users.createdAt, since)] : []\n\t\tconst purchaseConditions = [paidPurchase()]\n\t\tif (since) purchaseConditions.push(gte(purchases.createdAt, since))\n\n\t\tconst [userCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(users)\n\t\t\t.where(userConditions.length > 0 ? and(...userConditions) : undefined)\n\n\t\tconst [purchaseCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(purchases)\n\t\t\t.where(and(...purchaseConditions))\n\n\t\tconst [attributedCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(purchases)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\t...purchaseConditions,\n\t\t\t\t\tsql`(\n JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL\n OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL\n )`,\n\t\t\t\t),\n\t\t\t)\n\n\t\tconst totalSignups = userCount?.total ?? 0\n\t\tconst totalPurchases = purchaseCount?.total ?? 0\n\t\tconst attributedPurchases = attributedCount?.total ?? 0\n\n\t\treturn {\n\t\t\ttotalSignups,\n\t\t\ttotalPurchases,\n\t\t\tattributedPurchases,\n\t\t\tconversionRate: totalSignups > 0 ? totalPurchases / totalSignups : 0,\n\t\t\tattributionCoverage:\n\t\t\t\ttotalPurchases > 0 ? attributedPurchases / totalPurchases : 0,\n\t\t}\n\t}\n\n\tasync function getAttributedRevenueSummary(\n\t\trange: AnalyticsTimeRange = '30d',\n\t) {\n\t\tconst since = rangeToDate(range)\n\t\tconst conditions = [paidPurchase()]\n\t\tif (since) conditions.push(gte(purchases.createdAt, since))\n\n\t\tconst [totals] = await db\n\t\t\t.select({ total: sum(purchases.totalAmount), count: count() })\n\t\t\t.from(purchases)\n\t\t\t.where(and(...conditions))\n\n\t\tconst [attributed] = await db\n\t\t\t.select({ total: sum(purchases.totalAmount) })\n\t\t\t.from(purchases)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\t...conditions,\n\t\t\t\t\tsql`(\n JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL\n OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL\n )`,\n\t\t\t\t),\n\t\t\t)\n\n\t\tconst totalRevenue = Number(totals?.total ?? 0)\n\t\tconst attributedRevenue = Number(attributed?.total ?? 0)\n\t\tconst unattributedRevenue = totalRevenue - attributedRevenue\n\t\tconst totalPurchases = totals?.count ?? 0\n\n\t\treturn {\n\t\t\ttotalRevenue,\n\t\t\tattributedRevenue,\n\t\t\tunattributedRevenue,\n\t\t\tattributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,\n\t\t\ttotalPurchases,\n\t\t}\n\t}\n\n\tasync function getContentPurchaseCorrelation(\n\t\trange: AnalyticsTimeRange = '30d',\n\t\tlimit: number = 20,\n\t) {\n\t\tconst since = rangeToDate(range)\n\t\tconst purchaseConditions = [paidPurchase()]\n\t\tif (since) purchaseConditions.push(gte(purchases.createdAt, since))\n\n\t\tconst purchaserRows = await db\n\t\t\t.selectDistinct({ userId: purchases.userId })\n\t\t\t.from(purchases)\n\t\t\t.where(and(...purchaseConditions))\n\n\t\tconst purchaserIds = purchaserRows\n\t\t\t.map((r: any) => r.userId)\n\t\t\t.filter((id: any): id is string => id !== null)\n\n\t\tif (purchaserIds.length === 0) return []\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tresourceId: resourceProgress.resourceId,\n\t\t\t\tpurchaserCount: count(),\n\t\t\t})\n\t\t\t.from(resourceProgress)\n\t\t\t.where(\n\t\t\t\tsql`${resourceProgress.userId} IN (${sql.join(\n\t\t\t\t\tpurchaserIds.map((id: string) => sql`${id}`),\n\t\t\t\t\tsql`, `,\n\t\t\t\t)})`,\n\t\t\t)\n\t\t\t.groupBy(resourceProgress.resourceId)\n\t\t\t.orderBy(desc(count()))\n\t\t\t.limit(limit)\n\n\t\treturn rows.map((r: any) => ({\n\t\t\tresourceId: r.resourceId,\n\t\t\tpurchaserCount: r.purchaserCount,\n\t\t}))\n\t}\n\n\t// ─── Attribution Trail ───────────────────────────────────────────────────\n\n\t/**\n\t * Trace the full attribution journey for a user by email or purchaseId.\n\t * Walks: ShortlinkClick → ShortlinkAttribution (signup) →\n\t * ResourceProgress → Purchase\n\t */\n\tasync function traceAttribution(opts: {\n\t\temail?: string\n\t\tpurchaseId?: string\n\t}): Promise<AttributionTrail> {\n\t\tconst events: AttributionTrailEvent[] = []\n\n\t\t// Resolve user\n\t\tlet userId: string | null = null\n\t\tlet userEmail: string | null = opts.email ?? null\n\t\tlet userRecord: AttributionTrail['user'] = null\n\n\t\tif (opts.purchaseId) {\n\t\t\tconst [purchase] = await db\n\t\t\t\t.select({\n\t\t\t\t\tuserId: purchases.userId,\n\t\t\t\t\temail: users.email,\n\t\t\t\t})\n\t\t\t\t.from(purchases)\n\t\t\t\t.leftJoin(users, eq(purchases.userId, users.id))\n\t\t\t\t.where(eq(purchases.id, opts.purchaseId))\n\t\t\t\t.limit(1)\n\t\t\tuserId = purchase?.userId ?? null\n\t\t\tuserEmail = purchase?.email ?? userEmail\n\t\t}\n\n\t\tif (userEmail && !userId) {\n\t\t\tconst [u] = await db\n\t\t\t\t.select({ id: users.id })\n\t\t\t\t.from(users)\n\t\t\t\t.where(eq(users.email, userEmail))\n\t\t\t\t.limit(1)\n\t\t\tuserId = u?.id ?? null\n\t\t}\n\n\t\tif (userId) {\n\t\t\tconst [u] = await db\n\t\t\t\t.select({\n\t\t\t\t\tid: users.id,\n\t\t\t\t\temail: users.email,\n\t\t\t\t\tname: users.name,\n\t\t\t\t\tcreatedAt: users.createdAt,\n\t\t\t\t})\n\t\t\t\t.from(users)\n\t\t\t\t.where(eq(users.id, userId))\n\t\t\t\t.limit(1)\n\t\t\tuserRecord = u\n\t\t\t\t? {\n\t\t\t\t\t\tid: u.id,\n\t\t\t\t\t\temail: u.email,\n\t\t\t\t\t\tname: u.name ?? null,\n\t\t\t\t\t\tcreatedAt: u.createdAt!,\n\t\t\t\t\t}\n\t\t\t\t: null\n\t\t}\n\n\t\t// Find shortlink attributions for this user (by userId or email)\n\t\tconst attrConditions: any[] = []\n\t\tif (userId) attrConditions.push(eq(shortlinkAttribution.userId, userId))\n\t\tif (userEmail)\n\t\t\tattrConditions.push(eq(shortlinkAttribution.email, userEmail))\n\n\t\tif (attrConditions.length > 0) {\n\t\t\tconst attrs = await db\n\t\t\t\t.select({\n\t\t\t\t\ttype: shortlinkAttribution.type,\n\t\t\t\t\tcreatedAt: shortlinkAttribution.createdAt,\n\t\t\t\t\tmetadata: shortlinkAttribution.metadata,\n\t\t\t\t\tshortlinkId: shortlinkAttribution.shortlinkId,\n\t\t\t\t\tslug: shortlink.slug,\n\t\t\t\t\turl: shortlink.url,\n\t\t\t\t})\n\t\t\t\t.from(shortlinkAttribution)\n\t\t\t\t.leftJoin(shortlink, eq(shortlinkAttribution.shortlinkId, shortlink.id))\n\t\t\t\t.where(sql`(${sql.join(attrConditions, sql` OR `)})`)\n\t\t\t\t.orderBy(shortlinkAttribution.createdAt)\n\n\t\t\tfor (const attr of attrs) {\n\t\t\t\t// Find clicks on this shortlink before the attribution event\n\t\t\t\tconst clicks = await db\n\t\t\t\t\t.select({\n\t\t\t\t\t\ttimestamp: shortlinkClick.timestamp,\n\t\t\t\t\t\treferrer: shortlinkClick.referrer,\n\t\t\t\t\t\tcountry: shortlinkClick.country,\n\t\t\t\t\t\tdevice: shortlinkClick.device,\n\t\t\t\t\t})\n\t\t\t\t\t.from(shortlinkClick)\n\t\t\t\t\t.where(\n\t\t\t\t\t\tand(\n\t\t\t\t\t\t\teq(shortlinkClick.shortlinkId, attr.shortlinkId),\n\t\t\t\t\t\t\tlte(shortlinkClick.timestamp, attr.createdAt),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t\t.orderBy(desc(shortlinkClick.timestamp))\n\t\t\t\t\t.limit(3) // last 3 clicks before attribution\n\n\t\t\t\tfor (const click of clicks) {\n\t\t\t\t\tevents.push({\n\t\t\t\t\t\ttype: 'click',\n\t\t\t\t\t\ttimestamp: click.timestamp,\n\t\t\t\t\t\tdetail: {\n\t\t\t\t\t\t\tshortlink: `/s/${attr.slug}`,\n\t\t\t\t\t\t\tdestination: attr.url,\n\t\t\t\t\t\t\treferrer: click.referrer,\n\t\t\t\t\t\t\tcountry: click.country,\n\t\t\t\t\t\t\tdevice: click.device,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tevents.push({\n\t\t\t\t\ttype: attr.type === 'purchase' ? 'purchase' : 'signup',\n\t\t\t\t\ttimestamp: attr.createdAt,\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tshortlink: `/s/${attr.slug}`,\n\t\t\t\t\t\tmetadata: attr.metadata ? JSON.parse(attr.metadata) : null,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Find resource progress for this user\n\t\tif (userId) {\n\t\t\tconst progress = await db\n\t\t\t\t.select({\n\t\t\t\t\tresourceId: resourceProgress.resourceId,\n\t\t\t\t\tcompletedAt: resourceProgress.completedAt,\n\t\t\t\t\tcreatedAt: resourceProgress.createdAt,\n\t\t\t\t})\n\t\t\t\t.from(resourceProgress)\n\t\t\t\t.where(eq(resourceProgress.userId, userId))\n\t\t\t\t.orderBy(resourceProgress.createdAt)\n\t\t\t\t.limit(20) // cap at 20 most recent\n\n\t\t\tfor (const p of progress) {\n\t\t\t\tevents.push({\n\t\t\t\t\ttype: 'progress',\n\t\t\t\t\ttimestamp: p.completedAt ?? p.createdAt,\n\t\t\t\t\tdetail: { resourceId: p.resourceId },\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Find purchases\n\t\tconst purchaseConditions = [paidPurchase()]\n\t\tif (opts.purchaseId) {\n\t\t\tpurchaseConditions.push(eq(purchases.id, opts.purchaseId))\n\t\t} else if (userId) {\n\t\t\tpurchaseConditions.push(eq(purchases.userId, userId))\n\t\t} else {\n\t\t\t// No user found, return empty\n\t\t\treturn { user: userRecord, events: [], purchases: [] }\n\t\t}\n\n\t\tconst purchaseRows = await db\n\t\t\t.select({\n\t\t\t\tid: purchases.id,\n\t\t\t\ttotalAmount: purchases.totalAmount,\n\t\t\t\tproductName: products.name,\n\t\t\t\tcreatedAt: purchases.createdAt,\n\t\t\t\tcountry: purchases.country,\n\t\t\t\tfields: purchases.fields,\n\t\t\t})\n\t\t\t.from(purchases)\n\t\t\t.leftJoin(products, eq(purchases.productId, products.id))\n\t\t\t.where(and(...purchaseConditions))\n\t\t\t.orderBy(purchases.createdAt)\n\n\t\tconst purchaseResults = purchaseRows.map((p: any) => {\n\t\t\tconst fields = (p.fields as Record<string, any>) ?? {}\n\t\t\tevents.push({\n\t\t\t\ttype: 'purchase',\n\t\t\t\ttimestamp: p.createdAt,\n\t\t\t\tdetail: {\n\t\t\t\t\tpurchaseId: p.id,\n\t\t\t\t\tamount: Number(p.totalAmount),\n\t\t\t\t\tproduct: p.productName,\n\t\t\t\t},\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tid: p.id,\n\t\t\t\ttotalAmount: Number(p.totalAmount),\n\t\t\t\tproductName: p.productName ?? 'Unknown',\n\t\t\t\tcreatedAt: p.createdAt,\n\t\t\t\tcountry: p.country,\n\t\t\t\tutmSource: fields.utmSource ?? null,\n\t\t\t\tutmMedium: fields.utmMedium ?? null,\n\t\t\t\tutmCampaign: fields.utmCampaign ?? null,\n\t\t\t}\n\t\t})\n\n\t\t// Sort all events by timestamp\n\t\tevents.sort(\n\t\t\t(a, b) =>\n\t\t\t\tnew Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),\n\t\t)\n\n\t\treturn { user: userRecord, events, purchases: purchaseResults }\n\t}\n\n\treturn {\n\t\tgetRevenueSummary,\n\t\tgetRevenueByDay,\n\t\tgetPreviousPeriodRevenueByDay,\n\t\tgetRevenueByProduct,\n\t\tgetRevenueByCountry,\n\t\tgetRecentPurchases,\n\t\tgetAttributionSummary,\n\t\tgetShortlinkPerformance,\n\t\tgetRevenueBySource,\n\t\tgetConversionFunnel,\n\t\tgetAttributedRevenueSummary,\n\t\tgetContentPurchaseCorrelation,\n\t\ttraceAttribution,\n\t}\n}\n\nexport type DatabaseAnalyticsProvider = ReturnType<\n\ttypeof createDatabaseProvider\n>\n","import { BetaAnalyticsDataClient } from '@google-analytics/data'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type GA4TimeRange = '24h' | '7d' | '30d' | '90d'\n\nexport interface GA4ProviderConfig {\n\tpropertyId: string\n\tclientEmail: string\n\tprivateKey: string\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a GA4 analytics provider with injected credentials.\n * Lazily initializes the BetaAnalyticsDataClient on first use.\n *\n * @param config - GA4 credentials and property configuration\n */\nexport function createGA4Provider(config: GA4ProviderConfig) {\n\tconst { propertyId, clientEmail, privateKey } = config\n\n\tlet _client: BetaAnalyticsDataClient | null = null\n\n\tfunction getClient(): BetaAnalyticsDataClient {\n\t\tif (!_client) {\n\t\t\t_client = new BetaAnalyticsDataClient({\n\t\t\t\tcredentials: {\n\t\t\t\t\tclient_email: clientEmail,\n\t\t\t\t\tprivate_key: privateKey.replace(/\\\\n/g, '\\n'),\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t\treturn _client\n\t}\n\n\tfunction rangeToDateRange(range: GA4TimeRange) {\n\t\tconst map: Record<GA4TimeRange, string> = {\n\t\t\t'24h': '1daysAgo',\n\t\t\t'7d': '7daysAgo',\n\t\t\t'30d': '30daysAgo',\n\t\t\t'90d': '90daysAgo',\n\t\t}\n\t\treturn { startDate: map[range], endDate: 'today' }\n\t}\n\n\t// ─── Reports ──────────────────────────────────────────────────────────────\n\n\tasync function getTrafficOverview(range: GA4TimeRange = '30d') {\n\t\tconst client = getClient()\n\n\t\tconst [response] = await client.runReport({\n\t\t\tproperty: `properties/${propertyId}`,\n\t\t\tdateRanges: [rangeToDateRange(range)],\n\t\t\tmetrics: [\n\t\t\t\t{ name: 'sessions' },\n\t\t\t\t{ name: 'totalUsers' },\n\t\t\t\t{ name: 'newUsers' },\n\t\t\t\t{ name: 'screenPageViews' },\n\t\t\t\t{ name: 'averageSessionDuration' },\n\t\t\t\t{ name: 'bounceRate' },\n\t\t\t],\n\t\t})\n\n\t\tconst row = response?.rows?.[0]\n\t\tif (!row) {\n\t\t\treturn {\n\t\t\t\tsessions: 0,\n\t\t\t\ttotalUsers: 0,\n\t\t\t\tnewUsers: 0,\n\t\t\t\tpageviews: 0,\n\t\t\t\tavgSessionDuration: 0,\n\t\t\t\tbounceRate: 0,\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tsessions: Number(row.metricValues?.[0]?.value ?? 0),\n\t\t\ttotalUsers: Number(row.metricValues?.[1]?.value ?? 0),\n\t\t\tnewUsers: Number(row.metricValues?.[2]?.value ?? 0),\n\t\t\tpageviews: Number(row.metricValues?.[3]?.value ?? 0),\n\t\t\tavgSessionDuration: Number(row.metricValues?.[4]?.value ?? 0),\n\t\t\tbounceRate: Number(row.metricValues?.[5]?.value ?? 0),\n\t\t}\n\t}\n\n\tasync function getTopPages(range: GA4TimeRange = '30d', limit = 20) {\n\t\tconst client = getClient()\n\n\t\tconst [response] = await client.runReport({\n\t\t\tproperty: `properties/${propertyId}`,\n\t\t\tdateRanges: [rangeToDateRange(range)],\n\t\t\tdimensions: [{ name: 'pagePath' }],\n\t\t\tmetrics: [\n\t\t\t\t{ name: 'screenPageViews' },\n\t\t\t\t{ name: 'totalUsers' },\n\t\t\t\t{ name: 'averageSessionDuration' },\n\t\t\t],\n\t\t\torderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }],\n\t\t\tlimit,\n\t\t})\n\n\t\treturn (\n\t\t\tresponse?.rows?.map((row) => ({\n\t\t\t\tpath: row.dimensionValues?.[0]?.value ?? '',\n\t\t\t\tpageviews: Number(row.metricValues?.[0]?.value ?? 0),\n\t\t\t\tusers: Number(row.metricValues?.[1]?.value ?? 0),\n\t\t\t\tavgDuration: Number(row.metricValues?.[2]?.value ?? 0),\n\t\t\t})) ?? []\n\t\t)\n\t}\n\n\tasync function getTrafficSources(range: GA4TimeRange = '30d', limit = 15) {\n\t\tconst client = getClient()\n\n\t\tconst [response] = await client.runReport({\n\t\t\tproperty: `properties/${propertyId}`,\n\t\t\tdateRanges: [rangeToDateRange(range)],\n\t\t\tdimensions: [{ name: 'sessionSource' }, { name: 'sessionMedium' }],\n\t\t\tmetrics: [{ name: 'sessions' }, { name: 'totalUsers' }],\n\t\t\torderBys: [{ metric: { metricName: 'sessions' }, desc: true }],\n\t\t\tlimit,\n\t\t})\n\n\t\treturn (\n\t\t\tresponse?.rows?.map((row) => ({\n\t\t\t\tsource: row.dimensionValues?.[0]?.value ?? '(direct)',\n\t\t\t\tmedium: row.dimensionValues?.[1]?.value ?? '(none)',\n\t\t\t\tsessions: Number(row.metricValues?.[0]?.value ?? 0),\n\t\t\t\tusers: Number(row.metricValues?.[1]?.value ?? 0),\n\t\t\t})) ?? []\n\t\t)\n\t}\n\n\tasync function getSessionsByDay(range: GA4TimeRange = '30d') {\n\t\tconst client = getClient()\n\n\t\tconst [response] = await client.runReport({\n\t\t\tproperty: `properties/${propertyId}`,\n\t\t\tdateRanges: [rangeToDateRange(range)],\n\t\t\tdimensions: [{ name: 'date' }],\n\t\t\tmetrics: [\n\t\t\t\t{ name: 'sessions' },\n\t\t\t\t{ name: 'totalUsers' },\n\t\t\t\t{ name: 'screenPageViews' },\n\t\t\t],\n\t\t\torderBys: [{ dimension: { dimensionName: 'date' }, desc: false }],\n\t\t})\n\n\t\treturn (\n\t\t\tresponse?.rows?.map((row) => {\n\t\t\t\tconst raw = row.dimensionValues?.[0]?.value ?? ''\n\t\t\t\tconst date = `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`\n\t\t\t\treturn {\n\t\t\t\t\tdate,\n\t\t\t\t\tsessions: Number(row.metricValues?.[0]?.value ?? 0),\n\t\t\t\t\tusers: Number(row.metricValues?.[1]?.value ?? 0),\n\t\t\t\t\tpageviews: Number(row.metricValues?.[2]?.value ?? 0),\n\t\t\t\t}\n\t\t\t}) ?? []\n\t\t)\n\t}\n\n\treturn {\n\t\tgetTrafficOverview,\n\t\tgetTopPages,\n\t\tgetTrafficSources,\n\t\tgetSessionsByDay,\n\t}\n}\n\nexport type GA4AnalyticsProvider = ReturnType<typeof createGA4Provider>\n","import type { GA4TimeRange } from './ga4'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all'\n\nexport interface TrafficRevenueCorrelation {\n\ttraffic: {\n\t\tdate: string\n\t\tsessions: number\n\t\tusers: number\n\t\tpageviews: number\n\t}[]\n\trevenue: {\n\t\tdate: string\n\t\trevenue: number\n\t\tcount: number\n\t}[]\n}\n\nexport interface DerivedProviderDeps {\n\tdatabase: {\n\t\tgetRevenueByDay: (range: AnalyticsRange) => Promise<\n\t\t\t{\n\t\t\t\tdate: string\n\t\t\t\trevenue: number\n\t\t\t\tcount: number\n\t\t\t}[]\n\t\t>\n\t}\n\tga4: {\n\t\tgetSessionsByDay: (range: GA4TimeRange) => Promise<\n\t\t\t{\n\t\t\t\tdate: string\n\t\t\t\tsessions: number\n\t\t\t\tusers: number\n\t\t\t\tpageviews: number\n\t\t\t}[]\n\t\t>\n\t}\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a derived analytics provider that combines data from multiple\n * sources (database + GA4) to compute correlation metrics.\n *\n * @param deps - Provider dependencies (database and ga4 providers)\n */\nexport function createDerivedProvider(deps: DerivedProviderDeps) {\n\tconst { database, ga4 } = deps\n\n\tfunction toGA4Range(range: AnalyticsRange): GA4TimeRange {\n\t\tif (range === 'all') return '90d'\n\t\treturn range\n\t}\n\n\t/**\n\t * Correlates GA4 traffic sessions by day with revenue by day,\n\t * enabling side-by-side analysis of traffic and revenue trends.\n\t */\n\tasync function getTrafficRevenueCorrelation(\n\t\trange: AnalyticsRange,\n\t): Promise<TrafficRevenueCorrelation> {\n\t\tconst [traffic, revenue] = await Promise.all([\n\t\t\tga4.getSessionsByDay(toGA4Range(range)),\n\t\t\tdatabase.getRevenueByDay(range),\n\t\t])\n\n\t\treturn { traffic, revenue }\n\t}\n\n\treturn {\n\t\tgetTrafficRevenueCorrelation,\n\t}\n}\n\nexport type DerivedAnalyticsProvider = ReturnType<typeof createDerivedProvider>\n","import { sql } from 'drizzle-orm'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type TimeRange = '7:days' | '30:days' | '90:days'\n\n/** Ranges available on the Top Videos table (independent from page-level range) */\nexport type VideoTableRange = '24:hours' | '7:days' | '30:days' | '90:days'\n\nexport interface MuxOverallResponse {\n\tdata: {\n\t\tvalue: number\n\t\ttotal_watch_time: number\n\t\ttotal_playing_time: number\n\t\ttotal_views: number\n\t\tglobal_value: number | null\n\t}\n\ttimeframe: [number, number]\n}\n\nexport interface MuxTimeseriesResponse {\n\tdata: [string, number | null, number | null][]\n\ttimeframe: [number, number]\n\ttotal_row_count: number\n}\n\nexport interface MuxBreakdownItem {\n\tviews: number\n\tvalue: number\n\ttotal_watch_time: number\n\ttotal_playing_time: number\n\tnegative_impact: number | null\n\tfield: string\n}\n\nexport interface MuxBreakdownResponse {\n\tdata: MuxBreakdownItem[]\n\ttimeframe: [number, number]\n\ttotal_row_count: number\n}\n\nexport interface VideoDashboardData {\n\toverview: {\n\t\ttotalViews: number\n\t\tuniqueViewers: number\n\t\ttotalWatchTimeMs: number\n\t\ttotalPlayingTimeMs: number\n\t\tviewerExperienceScore: number\n\t\tglobalExperienceScore: number | null\n\t}\n\twatchTimeSeries: {\n\t\tdate: string\n\t\twatchTimeMs: number\n\t}[]\n\ttopVideos: {\n\t\ttitle: string\n\t\tviews: number\n\t\twatchTimeMs: number\n\t\tplayingTimeMs: number\n\t}[]\n\tcountries: {\n\t\tcountry: string\n\t\tviews: number\n\t\twatchTimeMs: number\n\t}[]\n}\n\nexport type VideoDetailBreakdowns = {\n\tcountries: {\n\t\tcountry: string\n\t\tviews: number\n\t\twatchTimeMs: number\n\t}[]\n\ttimeseries: {\n\t\tdate: string\n\t\tviews: number\n\t}[]\n}\n\nexport interface MuxProviderConfig {\n\ttokenId: string\n\ttokenSecret: string\n}\n\nexport interface MuxDbDeps {\n\tdb: any\n\tcontentResource: any\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a Mux analytics provider with injected credentials.\n * Optionally accepts db dependencies for thumbnail lookups that\n * require querying ContentResource records.\n *\n * @param config - Mux Data API token configuration\n * @param dbDeps - Optional drizzle db + contentResource table for thumbnail queries\n */\nexport function createMuxProvider(\n\tconfig: MuxProviderConfig,\n\tdbDeps?: MuxDbDeps,\n) {\n\tconst MUX_DATA_BASE = 'https://api.mux.com/data/v1'\n\n\tfunction getAuthHeader(): string {\n\t\treturn `Basic ${Buffer.from(\n\t\t\t`${config.tokenId}:${config.tokenSecret}`,\n\t\t).toString('base64')}`\n\t}\n\n\tasync function muxDataFetch<T>(\n\t\tpath: string,\n\t\tparams?: Record<string, string | string[]>,\n\t): Promise<T> {\n\t\tconst url = new URL(`${MUX_DATA_BASE}${path}`)\n\t\tif (params) {\n\t\t\tfor (const [key, value] of Object.entries(params)) {\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) {\n\t\t\t\t\t\turl.searchParams.append(key, v)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\turl.searchParams.set(key, value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst response = await fetch(url.toString(), {\n\t\t\theaders: {\n\t\t\t\tAuthorization: getAuthHeader(),\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tnext: { revalidate: 300 }, // cache 5 minutes\n\t\t} as RequestInit)\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Mux Data API error: ${response.status} ${response.statusText}`,\n\t\t\t)\n\t\t}\n\n\t\treturn response.json() as Promise<T>\n\t}\n\n\t// ─── Comparison (unique viewers) ─────────────────────────────────────────\n\n\tinterface MuxComparisonItem {\n\t\tname: string\n\t\twatch_time?: number\n\t\tview_count?: number\n\t\tunique_viewers?: number\n\t\tstarted_views?: number\n\t\tended_views?: number\n\t\t[key: string]: unknown\n\t}\n\n\tinterface MuxComparisonResponse {\n\t\tdata: MuxComparisonItem[]\n\t\ttimeframe: [number, number]\n\t\ttotal_row_count: number | null\n\t}\n\n\tasync function getComparisonTotals(timeRange: TimeRange = '30:days') {\n\t\tconst resp = await muxDataFetch<MuxComparisonResponse>(\n\t\t\t'/metrics/comparison',\n\t\t\t{ 'timeframe[]': timeRange },\n\t\t)\n\t\tconst totals = resp.data.find((d) => d.name === 'totals')\n\t\treturn {\n\t\t\tuniqueViewers: totals?.unique_viewers ?? 0,\n\t\t\tviewCount: totals?.view_count ?? 0,\n\t\t\twatchTimeMs: totals?.watch_time ?? 0,\n\t\t}\n\t}\n\n\t// ─── API Functions ────────────────────────────────────────────────────────\n\n\tasync function getViewsOverall(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxOverallResponse>('/metrics/views/overall', {\n\t\t\t'timeframe[]': timeRange,\n\t\t})\n\t}\n\n\tasync function getViewerExperienceScore(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxOverallResponse>(\n\t\t\t'/metrics/viewer_experience_score/overall',\n\t\t\t{ 'timeframe[]': timeRange },\n\t\t)\n\t}\n\n\tasync function getViewsTimeseries(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxTimeseriesResponse>('/metrics/views/timeseries', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'day',\n\t\t})\n\t}\n\n\t/**\n\t * Fetch watch time (playing_time) timeseries grouped by day.\n\t * Mux returns [date, totalPlayingTimeMs, viewCount] tuples.\n\t * The value IS the total playing time in ms (not the average).\n\t */\n\tasync function getWatchTimeTimeseries(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxTimeseriesResponse>(\n\t\t\t'/metrics/playing_time/timeseries',\n\t\t\t{\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'day',\n\t\t\t},\n\t\t)\n\t}\n\n\tasync function getVideoBreakdown(\n\t\ttimeRange: TimeRange = '30:days',\n\t\tlimit: number = 25,\n\t) {\n\t\treturn muxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'video_title',\n\t\t\torder_by: 'views',\n\t\t\torder_direction: 'desc',\n\t\t\tlimit: String(limit),\n\t\t})\n\t}\n\n\t/**\n\t * Standalone video breakdown fetcher for the Top Videos table's\n\t * independent time-range tabs. Accepts VideoTableRange.\n\t */\n\tasync function getVideoBreakdownForRange(\n\t\ttimeRange: VideoTableRange,\n\t\tlimit: number = 50,\n\t) {\n\t\treturn muxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'video_title',\n\t\t\torder_by: 'views',\n\t\t\torder_direction: 'desc',\n\t\t\tlimit: String(limit),\n\t\t})\n\t}\n\n\tasync function getCountryBreakdown(\n\t\ttimeRange: TimeRange = '30:days',\n\t\tlimit: number = 10,\n\t) {\n\t\treturn muxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'country',\n\t\t\torder_by: 'views',\n\t\t\torder_direction: 'desc',\n\t\t\tlimit: String(limit),\n\t\t})\n\t}\n\n\tasync function getVideoDetailBreakdowns(\n\t\tvideoTitle: string,\n\t\ttimeRange: TimeRange = '30:days',\n\t): Promise<VideoDetailBreakdowns> {\n\t\tconst filter = `video_title:${videoTitle}`\n\t\tconst [countries, timeseries] = await Promise.all([\n\t\t\tmuxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'country',\n\t\t\t\torder_by: 'views',\n\t\t\t\torder_direction: 'desc',\n\t\t\t\tlimit: '8',\n\t\t\t\t'filters[]': filter,\n\t\t\t}),\n\t\t\tmuxDataFetch<MuxTimeseriesResponse>('/metrics/views/timeseries', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'day',\n\t\t\t\t'filters[]': filter,\n\t\t\t}),\n\t\t])\n\n\t\treturn {\n\t\t\tcountries: countries.data.map((c) => ({\n\t\t\t\tcountry: c.field,\n\t\t\t\tviews: c.views,\n\t\t\t\twatchTimeMs: c.total_watch_time,\n\t\t\t})),\n\t\t\ttimeseries: timeseries.data.map(([date, value]) => ({\n\t\t\t\tdate,\n\t\t\t\tviews: value ?? 0,\n\t\t\t})),\n\t\t}\n\t}\n\n\t// ─── Aggregate fetcher ────────────────────────────────────────────────────\n\n\tasync function getVideoDashboardData(\n\t\ttimeRange: TimeRange = '30:days',\n\t): Promise<VideoDashboardData> {\n\t\tconst [views, experience, comparison, watchTime, videos, countries] =\n\t\t\tawait Promise.all([\n\t\t\t\tgetViewsOverall(timeRange),\n\t\t\t\tgetViewerExperienceScore(timeRange),\n\t\t\t\tgetComparisonTotals(timeRange),\n\t\t\t\tgetWatchTimeTimeseries(timeRange),\n\t\t\t\tgetVideoBreakdown(timeRange, 50),\n\t\t\t\tgetCountryBreakdown(timeRange, 15),\n\t\t\t])\n\n\t\treturn {\n\t\t\toverview: {\n\t\t\t\ttotalViews: views.data.total_views,\n\t\t\t\tuniqueViewers: comparison.uniqueViewers,\n\t\t\t\ttotalWatchTimeMs: views.data.total_watch_time,\n\t\t\t\ttotalPlayingTimeMs: views.data.total_playing_time,\n\t\t\t\tviewerExperienceScore: experience.data.value,\n\t\t\t\tglobalExperienceScore: experience.data.global_value,\n\t\t\t},\n\t\t\twatchTimeSeries: watchTime.data.map(([date, totalMs]) => ({\n\t\t\t\tdate,\n\t\t\t\t// Mux playing_time timeseries: value is total playing time in ms\n\t\t\t\twatchTimeMs: totalMs ?? 0,\n\t\t\t})),\n\t\t\ttopVideos: videos.data\n\t\t\t\t.filter((v) => v.field !== '')\n\t\t\t\t.map((v) => ({\n\t\t\t\t\ttitle: v.field,\n\t\t\t\t\tviews: v.views,\n\t\t\t\t\twatchTimeMs: v.total_watch_time,\n\t\t\t\t\tplayingTimeMs: v.total_playing_time,\n\t\t\t\t})),\n\t\t\tcountries: countries.data.map((c) => ({\n\t\t\t\tcountry: c.field,\n\t\t\t\tviews: c.views,\n\t\t\t\twatchTimeMs: c.total_watch_time,\n\t\t\t})),\n\t\t}\n\t}\n\n\t/**\n\t * Resolve video titles to Mux thumbnail URLs.\n\t * 1. Break down by video_id to get ContentResource IDs for top videos\n\t * 2. Look up playback IDs from ContentResource.fields.muxPlaybackId\n\t * 3. Build image.mux.com thumbnail URLs\n\t * Returns Record<title, thumbnailUrl>.\n\t * Requires dbDeps to be provided at factory construction time.\n\t */\n\tasync function getVideoThumbnails(\n\t\ttimeRange: TimeRange = '30:days',\n\t\tlimit: number = 10,\n\t): Promise<Record<string, string>> {\n\t\tif (!dbDeps) {\n\t\t\tthrow new Error(\n\t\t\t\t'getVideoThumbnails requires dbDeps (db + contentResource) ' +\n\t\t\t\t\t'to be provided to createMuxProvider',\n\t\t\t)\n\t\t}\n\n\t\tconst { db, contentResource } = dbDeps\n\n\t\t// Get breakdown by both video_id and video_title\n\t\tconst [byId, byTitle] = await Promise.all([\n\t\t\tmuxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'video_id',\n\t\t\t\torder_by: 'views',\n\t\t\t\torder_direction: 'desc',\n\t\t\t\tlimit: String(limit),\n\t\t\t}),\n\t\t\tmuxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'video_title',\n\t\t\t\torder_by: 'views',\n\t\t\t\torder_direction: 'desc',\n\t\t\t\tlimit: String(limit),\n\t\t\t}),\n\t\t])\n\n\t\t// video_id breakdown gives ContentResource IDs — look up playback IDs\n\t\tconst videoIds = byId.data\n\t\t\t.filter((v) => v.field && v.field !== '')\n\t\t\t.map((v) => v.field)\n\n\t\tif (videoIds.length === 0) return {}\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tid: contentResource.id,\n\t\t\t\tplaybackId:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.muxPlaybackId'))`.as(\n\t\t\t\t\t\t'playbackId',\n\t\t\t\t\t),\n\t\t\t})\n\t\t\t.from(contentResource)\n\t\t\t.where(\n\t\t\t\tsql`${contentResource.id} IN (${sql.join(\n\t\t\t\t\tvideoIds.map((id: string) => sql`${id}`),\n\t\t\t\t\tsql`, `,\n\t\t\t\t)})`,\n\t\t\t)\n\n\t\t// Map video_id → playbackId\n\t\tconst idToPlayback = new Map<string, string>()\n\t\tfor (const r of rows) {\n\t\t\tif (r.playbackId && r.playbackId !== 'null') {\n\t\t\t\tidToPlayback.set(r.id, r.playbackId)\n\t\t\t}\n\t\t}\n\n\t\t// Match by position: byId and byTitle are both sorted by views desc,\n\t\t// so position i in byId corresponds to position i in byTitle\n\t\tconst result: Record<string, string> = {}\n\t\tfor (let i = 0; i < Math.min(byId.data.length, byTitle.data.length); i++) {\n\t\t\tconst videoId = byId.data[i]?.field\n\t\t\tconst title = byTitle.data[i]?.field\n\t\t\tif (!videoId || !title) continue\n\t\t\tconst playbackId = idToPlayback.get(videoId)\n\t\t\tif (playbackId) {\n\t\t\t\tresult[title] =\n\t\t\t\t\t`https://image.mux.com/${playbackId}/thumbnail.jpg?width=240&height=135&fit_mode=smartcrop`\n\t\t\t}\n\t\t}\n\n\t\treturn result\n\t}\n\n\treturn {\n\t\tgetComparisonTotals,\n\t\tgetViewsOverall,\n\t\tgetViewerExperienceScore,\n\t\tgetViewsTimeseries,\n\t\tgetWatchTimeTimeseries,\n\t\tgetVideoBreakdown,\n\t\tgetVideoBreakdownForRange,\n\t\tgetCountryBreakdown,\n\t\tgetVideoDetailBreakdowns,\n\t\tgetVideoDashboardData,\n\t\tgetVideoThumbnails,\n\t}\n}\n\nexport type MuxAnalyticsProvider = ReturnType<typeof createMuxProvider>\n","import { and, count, eq, sql } from 'drizzle-orm'\n\nimport type { AnalyticsRange } from '../types'\n\n// ─── Schema types ────────────────────────────────────────────────────────────\n\n/**\n * Minimal column shape required from the contentResource table.\n * We only reference the columns we actually query against.\n */\ninterface ContentResourceTable {\n\tid: any\n\ttype: any\n\tfields: any\n}\n\n/**\n * Minimal column shape required from the contentResourceResource table.\n */\ninterface ContentResourceResourceTable {\n\tresourceOfId: any\n\tresourceId: any\n}\n\n/**\n * Minimal column shape required from the questionResponse table.\n */\ninterface QuestionResponseTable {\n\tid: any\n\tsurveyId: any\n\tquestionId: any\n\trespondentKey: any\n\tsurveySessionId: any\n\tuserId: any\n\temailListSubscriberId: any\n\tcreatedAt: any\n\tupdatedAt: any\n\tfields: any\n}\n\n/**\n * Minimal column shape required from the users table.\n * Optional — only needed for getSurveyResponses to resolve user emails.\n */\ninterface UsersTable {\n\tid: any\n\temail: any\n}\n\nexport interface SurveyAnalyticsSchema {\n\tcontentResource: ContentResourceTable\n\tcontentResourceResource: ContentResourceResourceTable\n\tquestionResponse: QuestionResponseTable\n\tusers?: UsersTable\n}\n\ntype CanonicalSurveyRow = {\n\tresponseId: string\n\tsurveyId: string\n\tsurveyTitle: string | null\n\tsurveySlug: string | null\n\tquestionId: string\n\tquestionText: string | null\n\tquestionType: string | null\n\tanswer: string | null\n\trespondentKey: string\n\tsurveySessionId: string | null\n\tuserId: string | null\n\tuserEmail: string | null\n\temailListSubscriberId: string | null\n\tcreatedAt: Date | null\n\tupdatedAt: Date | null\n}\n\n// ─── Return type ─────────────────────────────────────────────────────────────\n\nexport interface SurveyAnalyticsProvider {\n\tgetSurveySummary: (range?: AnalyticsRange) => Promise<{\n\t\ttotalSurveys: number\n\t\ttotalResponses: number\n\t\tuniqueRespondents: number\n\t\tavgResponsesPerSurvey: number\n\t}>\n\n\tgetSurveyList: (range?: AnalyticsRange) => Promise<\n\t\tArray<{\n\t\t\tsurveyId: string\n\t\t\tsurveyTitle: string\n\t\t\tsurveySlug: string\n\t\t\tresponses: number\n\t\t\tuniqueRespondents: number\n\t\t\tquestionCount: number\n\t\t}>\n\t>\n\n\tgetSurveyResponsesByDay: (range?: AnalyticsRange) => Promise<\n\t\tArray<{\n\t\t\tdate: string\n\t\t\tresponses: number\n\t\t}>\n\t>\n\n\tgetSurveyQuestionBreakdown: (\n\t\trange?: AnalyticsRange,\n\t\tlimit?: number,\n\t) => Promise<\n\t\tArray<{\n\t\t\tquestionId: string\n\t\t\tquestion: string\n\t\t\ttype: string | null\n\t\t\tresponses: number\n\t\t\tuniqueRespondents: number\n\t\t\tanswerDistribution: Array<{ answer: string; count: number }>\n\t\t}>\n\t>\n\n\tgetSurveyResponses: (\n\t\trange?: AnalyticsRange,\n\t\tlimit?: number,\n\t) => Promise<\n\t\tArray<{\n\t\t\tresponseId: string\n\t\t\tsurveyId: string\n\t\t\tsurveyTitle: string\n\t\t\tsurveySlug: string\n\t\t\tquestionId: string\n\t\t\tquestionText: string\n\t\t\tquestionType: string | null\n\t\t\tanswer: string\n\t\t\tuserId: string | null\n\t\t\tuserEmail: string | null\n\t\t\temailListSubscriberId: string | null\n\t\t\tcreatedAt: string\n\t\t}>\n\t>\n}\n\nfunction normalizeRespondentKey(row: {\n\trespondentKey: string | null\n\tuserId: string | null\n\temailListSubscriberId: string | null\n\tsurveySessionId: string | null\n}) {\n\tif (row.respondentKey) return row.respondentKey\n\tif (row.userId) return `user:${row.userId}`\n\tif (row.emailListSubscriberId) {\n\t\treturn `subscriber:${row.emailListSubscriberId}`\n\t}\n\tif (row.surveySessionId) return `session:${row.surveySessionId}`\n\treturn null\n}\n\nfunction getRowTimestamp(row: {\n\tupdatedAt: Date | null\n\tcreatedAt: Date | null\n}) {\n\tconst date = row.updatedAt ?? row.createdAt\n\treturn date instanceof Date ? date.getTime() : 0\n}\n\nfunction sortByNewest<\n\tT extends { createdAt: Date | null; updatedAt: Date | null },\n>(rows: T[]) {\n\treturn [...rows].sort((a, b) => getRowTimestamp(b) - getRowTimestamp(a))\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a survey analytics provider bound to the given Drizzle db instance\n * and schema tables.\n *\n * @param db - Drizzle database instance\n * @param schema - Object containing the required table references\n */\nexport function createSurveyProvider(\n\tdb: any,\n\tschema: SurveyAnalyticsSchema,\n): SurveyAnalyticsProvider {\n\tconst { contentResource, contentResourceResource, questionResponse } = schema\n\n\t// ─── Range helpers ───────────────────────────────────────────────────────\n\n\tfunction rangeToInterval(range: AnalyticsRange): string {\n\t\tswitch (range) {\n\t\t\tcase '24h':\n\t\t\t\treturn '1 DAY'\n\t\t\tcase '7d':\n\t\t\t\treturn '7 DAY'\n\t\t\tcase '30d':\n\t\t\t\treturn '30 DAY'\n\t\t\tcase '90d':\n\t\t\t\treturn '90 DAY'\n\t\t\tcase 'all':\n\t\t\t\treturn '3650 DAY'\n\t\t}\n\t}\n\n\tfunction rangeWhere(range: AnalyticsRange, column: any) {\n\t\treturn sql`${column} >= DATE_SUB(NOW(), INTERVAL ${sql.raw(rangeToInterval(range))})`\n\t}\n\n\tasync function fetchCanonicalRows(range: AnalyticsRange) {\n\t\tconst { users } = schema\n\n\t\tconst rawRows = users\n\t\t\t? await db\n\t\t\t\t\t.select({\n\t\t\t\t\t\tresponseId: questionResponse.id,\n\t\t\t\t\t\tsurveyId: questionResponse.surveyId,\n\t\t\t\t\t\tsurveyTitle: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,\n\t\t\t\t\t\tsurveySlug: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,\n\t\t\t\t\t\tquestionId: questionResponse.questionId,\n\t\t\t\t\t\tquestionText: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,\n\t\t\t\t\t\tquestionType: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,\n\t\t\t\t\t\tanswer: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,\n\t\t\t\t\t\trespondentKey: questionResponse.respondentKey,\n\t\t\t\t\t\tsurveySessionId: questionResponse.surveySessionId,\n\t\t\t\t\t\tuserId: questionResponse.userId,\n\t\t\t\t\t\tuserEmail: users.email,\n\t\t\t\t\t\temailListSubscriberId: questionResponse.emailListSubscriberId,\n\t\t\t\t\t\tcreatedAt: questionResponse.createdAt,\n\t\t\t\t\t\tupdatedAt: questionResponse.updatedAt,\n\t\t\t\t\t})\n\t\t\t\t\t.from(questionResponse)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS survey_cr`,\n\t\t\t\t\t\tsql`survey_cr.id = ${questionResponse.surveyId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS question_cr`,\n\t\t\t\t\t\tsql`question_cr.id = ${questionResponse.questionId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.leftJoin(users, eq(questionResponse.userId, users.id))\n\t\t\t\t\t.where(rangeWhere(range, questionResponse.createdAt))\n\t\t\t: await db\n\t\t\t\t\t.select({\n\t\t\t\t\t\tresponseId: questionResponse.id,\n\t\t\t\t\t\tsurveyId: questionResponse.surveyId,\n\t\t\t\t\t\tsurveyTitle: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,\n\t\t\t\t\t\tsurveySlug: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,\n\t\t\t\t\t\tquestionId: questionResponse.questionId,\n\t\t\t\t\t\tquestionText: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,\n\t\t\t\t\t\tquestionType: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,\n\t\t\t\t\t\tanswer: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,\n\t\t\t\t\t\trespondentKey: questionResponse.respondentKey,\n\t\t\t\t\t\tsurveySessionId: questionResponse.surveySessionId,\n\t\t\t\t\t\tuserId: questionResponse.userId,\n\t\t\t\t\t\tuserEmail: sql<string | null>`NULL`,\n\t\t\t\t\t\temailListSubscriberId: questionResponse.emailListSubscriberId,\n\t\t\t\t\t\tcreatedAt: questionResponse.createdAt,\n\t\t\t\t\t\tupdatedAt: questionResponse.updatedAt,\n\t\t\t\t\t})\n\t\t\t\t\t.from(questionResponse)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS survey_cr`,\n\t\t\t\t\t\tsql`survey_cr.id = ${questionResponse.surveyId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.leftJoin(\n\t\t\t\t\t\tsql`${contentResource} AS question_cr`,\n\t\t\t\t\t\tsql`question_cr.id = ${questionResponse.questionId}`,\n\t\t\t\t\t)\n\t\t\t\t\t.where(rangeWhere(range, questionResponse.createdAt))\n\n\t\tconst latestByAnswer = new Map<string, CanonicalSurveyRow>()\n\n\t\tfor (const row of rawRows) {\n\t\t\tconst respondentKey = normalizeRespondentKey(row)\n\t\t\tif (!respondentKey) continue\n\n\t\t\tconst dedupeKey = `${row.surveyId}::${row.questionId}::${respondentKey}`\n\t\t\tconst current = latestByAnswer.get(dedupeKey)\n\n\t\t\tif (!current || getRowTimestamp(row) >= getRowTimestamp(current)) {\n\t\t\t\tlatestByAnswer.set(dedupeKey, {\n\t\t\t\t\t...row,\n\t\t\t\t\trespondentKey,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\treturn sortByNewest(Array.from(latestByAnswer.values()))\n\t}\n\n\t// ─── Survey Summary ──────────────────────────────────────────────────────\n\n\tasync function getSurveySummary(range: AnalyticsRange = '30d') {\n\t\tconst [surveyCount] = await db\n\t\t\t.select({ total: count() })\n\t\t\t.from(contentResource)\n\t\t\t.where(eq(contentResource.type, 'survey'))\n\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst respondentKeys = new Set(\n\t\t\tcanonicalRows.map((row) => row.respondentKey),\n\t\t)\n\t\tconst totalSurveys = surveyCount?.total ?? 0\n\t\tconst totalResponses = canonicalRows.length\n\n\t\treturn {\n\t\t\ttotalSurveys,\n\t\t\ttotalResponses,\n\t\t\tuniqueRespondents: respondentKeys.size,\n\t\t\tavgResponsesPerSurvey:\n\t\t\t\ttotalSurveys > 0 ? totalResponses / totalSurveys : 0,\n\t\t}\n\t}\n\n\t// ─── Survey List ─────────────────────────────────────────────────────────\n\n\tasync function getSurveyList(range: AnalyticsRange = '30d') {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst responsesBySurvey = new Map<\n\t\t\tstring,\n\t\t\t{ responses: number; respondents: Set<string> }\n\t\t>()\n\n\t\tfor (const row of canonicalRows) {\n\t\t\tconst current = responsesBySurvey.get(row.surveyId) ?? {\n\t\t\t\tresponses: 0,\n\t\t\t\trespondents: new Set<string>(),\n\t\t\t}\n\t\t\tcurrent.responses += 1\n\t\t\tcurrent.respondents.add(row.respondentKey)\n\t\t\tresponsesBySurvey.set(row.surveyId, current)\n\t\t}\n\n\t\tconst surveys = await db\n\t\t\t.select({\n\t\t\t\tsurveyId: contentResource.id,\n\t\t\t\tsurveyTitle: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.title'))`,\n\t\t\t\tsurveySlug: sql<string>`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.slug'))`,\n\t\t\t})\n\t\t\t.from(contentResource)\n\t\t\t.where(eq(contentResource.type, 'survey'))\n\n\t\tconst questionCounts = await db\n\t\t\t.select({\n\t\t\t\tsurveyId: contentResourceResource.resourceOfId,\n\t\t\t\tquestionCount: count(),\n\t\t\t})\n\t\t\t.from(contentResourceResource)\n\t\t\t.innerJoin(\n\t\t\t\tcontentResource,\n\t\t\t\tand(\n\t\t\t\t\teq(contentResourceResource.resourceId, contentResource.id),\n\t\t\t\t\teq(contentResource.type, 'question'),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.groupBy(contentResourceResource.resourceOfId)\n\n\t\tconst questionCountMap = new Map(\n\t\t\tquestionCounts.map((qc: { surveyId: string; questionCount: number }) => [\n\t\t\t\tqc.surveyId,\n\t\t\t\tqc.questionCount,\n\t\t\t]),\n\t\t)\n\n\t\treturn surveys\n\t\t\t.map(\n\t\t\t\t(s: {\n\t\t\t\t\tsurveyId: string\n\t\t\t\t\tsurveyTitle: string | null\n\t\t\t\t\tsurveySlug: string | null\n\t\t\t\t}) => {\n\t\t\t\t\tconst counts = responsesBySurvey.get(s.surveyId)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsurveyId: s.surveyId,\n\t\t\t\t\t\tsurveyTitle: s.surveyTitle ?? '',\n\t\t\t\t\t\tsurveySlug: s.surveySlug ?? '',\n\t\t\t\t\t\tresponses: counts?.responses ?? 0,\n\t\t\t\t\t\tuniqueRespondents: counts?.respondents.size ?? 0,\n\t\t\t\t\t\tquestionCount: questionCountMap.get(s.surveyId) ?? 0,\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t)\n\t\t\t.sort(\n\t\t\t\t(a: { responses: number }, b: { responses: number }) =>\n\t\t\t\t\tb.responses - a.responses,\n\t\t\t)\n\t}\n\n\t// ─── Daily Responses ─────────────────────────────────────────────────────\n\n\tasync function getSurveyResponsesByDay(range: AnalyticsRange = '30d') {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst grouped = new Map<string, number>()\n\n\t\tfor (const row of canonicalRows) {\n\t\t\tif (!(row.createdAt instanceof Date)) continue\n\t\t\tconst date = row.createdAt.toISOString().slice(0, 10)\n\t\t\tgrouped.set(date, (grouped.get(date) ?? 0) + 1)\n\t\t}\n\n\t\treturn Array.from(grouped.entries())\n\t\t\t.sort((entryA: [string, number], entryB: [string, number]) =>\n\t\t\t\tentryA[0].localeCompare(entryB[0]),\n\t\t\t)\n\t\t\t.map(([date, responses]) => ({ date, responses }))\n\t}\n\n\t// ─── Question Breakdown ──────────────────────────────────────────────────\n\n\tasync function getSurveyQuestionBreakdown(\n\t\trange: AnalyticsRange = '30d',\n\t\tlimit = 20,\n\t) {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\t\tconst grouped = new Map<\n\t\t\tstring,\n\t\t\t{\n\t\t\t\tquestionId: string\n\t\t\t\tquestion: string\n\t\t\t\ttype: string | null\n\t\t\t\tresponses: number\n\t\t\t\trespondents: Set<string>\n\t\t\t\tanswers: Map<string, number>\n\t\t\t}\n\t\t>()\n\n\t\tfor (const row of canonicalRows) {\n\t\t\tconst current = grouped.get(row.questionId) ?? {\n\t\t\t\tquestionId: row.questionId,\n\t\t\t\tquestion: row.questionText ?? '',\n\t\t\t\ttype: row.questionType ?? null,\n\t\t\t\tresponses: 0,\n\t\t\t\trespondents: new Set<string>(),\n\t\t\t\tanswers: new Map<string, number>(),\n\t\t\t}\n\n\t\t\tcurrent.responses += 1\n\t\t\tcurrent.respondents.add(row.respondentKey)\n\t\t\tconst answer = row.answer ?? '(no answer)'\n\t\t\tcurrent.answers.set(answer, (current.answers.get(answer) ?? 0) + 1)\n\t\t\tgrouped.set(row.questionId, current)\n\t\t}\n\n\t\treturn Array.from(grouped.values())\n\t\t\t.sort(\n\t\t\t\t(a: { responses: number }, b: { responses: number }) =>\n\t\t\t\t\tb.responses - a.responses,\n\t\t\t)\n\t\t\t.slice(0, limit)\n\t\t\t.map((entry) => ({\n\t\t\t\tquestionId: entry.questionId,\n\t\t\t\tquestion: entry.question,\n\t\t\t\ttype: entry.type,\n\t\t\t\tresponses: entry.responses,\n\t\t\t\tuniqueRespondents: entry.respondents.size,\n\t\t\t\tanswerDistribution: Array.from(entry.answers.entries())\n\t\t\t\t\t.sort((a: [string, number], b: [string, number]) => b[1] - a[1])\n\t\t\t\t\t.map(([answer, count]) => ({ answer, count })),\n\t\t\t}))\n\t}\n\n\t// ─── Individual Response Rows ────────────────────────────────────────────\n\n\tasync function getSurveyResponses(\n\t\trange: AnalyticsRange = '30d',\n\t\tlimit = 100,\n\t) {\n\t\tconst canonicalRows = await fetchCanonicalRows(range)\n\n\t\treturn canonicalRows.slice(0, limit).map((row) => ({\n\t\t\tresponseId: row.responseId,\n\t\t\tsurveyId: row.surveyId,\n\t\t\tsurveyTitle: row.surveyTitle ?? '',\n\t\t\tsurveySlug: row.surveySlug ?? '',\n\t\t\tquestionId: row.questionId,\n\t\t\tquestionText: row.questionText ?? '',\n\t\t\tquestionType: row.questionType ?? null,\n\t\t\tanswer: row.answer ?? '',\n\t\t\tuserId: row.userId ?? null,\n\t\t\tuserEmail: row.userEmail ?? null,\n\t\t\temailListSubscriberId: row.emailListSubscriberId ?? null,\n\t\t\tcreatedAt: row.createdAt ? String(row.createdAt) : '',\n\t\t}))\n\t}\n\n\treturn {\n\t\tgetSurveySummary,\n\t\tgetSurveyList,\n\t\tgetSurveyResponsesByDay,\n\t\tgetSurveyQuestionBreakdown,\n\t\tgetSurveyResponses,\n\t}\n}\n"],"mappings":"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","names":["and","count","desc","eq","gt","gte","inArray","lte","sql","sum","createDatabaseProvider","db","schema","purchases","products","users","coupon","resourceProgress","shortlink","shortlinkAttribution","shortlinkClick","PAID_STATUSES","paidPurchase","inArray","status","rangeToDate","range","now","Date","hours","getTime","getRevenueSummary","since","conditions","push","gte","createdAt","totals","select","totalRevenue","sum","totalAmount","purchaseCount","count","from","where","and","Number","avgOrderValue","getRevenueByDay","rows","date","sql","as","revenue","groupBy","orderBy","map","r","getPreviousPeriodRevenueByDay","periodMs","periodStart","prevStart","lte","getRevenueByProduct","productId","productName","name","leftJoin","eq","id","desc","getRevenueByCountry","country","limit","getRecentPurchases","filter","bulkCouponId","couponId","userId","userName","userEmail","email","organizationId","seats","maxUses","gt","isTeam","query","findMany","with","product","user","getAttributionSummary","type","length","undefined","getShortlinkPerformance","clickConditions","timestamp","shortlinkId","slug","url","clicks","innerJoin","attrConditions","attrRows","attrMap","Map","a","existing","get","signups","set","attr","getRevenueBySource","source","fields","medium","campaign","getConversionFunnel","userConditions","purchaseConditions","userCount","total","attributedCount","totalSignups","totalPurchases","attributedPurchases","conversionRate","attributionCoverage","getAttributedRevenueSummary","attributed","attributedRevenue","unattributedRevenue","attributionRate","getContentPurchaseCorrelation","purchaserRows","selectDistinct","purchaserIds","resourceId","purchaserCount","join","traceAttribution","opts","events","userRecord","purchaseId","purchase","u","attrs","metadata","referrer","device","click","detail","destination","JSON","parse","progress","completedAt","p","purchaseRows","purchaseResults","amount","utmSource","utmMedium","utmCampaign","sort","b","BetaAnalyticsDataClient","createGA4Provider","config","propertyId","clientEmail","privateKey","_client","getClient","BetaAnalyticsDataClient","credentials","client_email","private_key","replace","rangeToDateRange","range","map","startDate","endDate","getTrafficOverview","client","response","runReport","property","dateRanges","metrics","name","row","rows","sessions","totalUsers","newUsers","pageviews","avgSessionDuration","bounceRate","Number","metricValues","value","getTopPages","limit","dimensions","orderBys","metric","metricName","desc","path","dimensionValues","users","avgDuration","getTrafficSources","source","medium","getSessionsByDay","dimension","dimensionName","raw","date","slice","createDerivedProvider","deps","database","ga4","toGA4Range","range","getTrafficRevenueCorrelation","traffic","revenue","Promise","all","getSessionsByDay","getRevenueByDay","sql","createMuxProvider","config","dbDeps","MUX_DATA_BASE","getAuthHeader","Buffer","from","tokenId","tokenSecret","toString","muxDataFetch","path","params","url","URL","key","value","Object","entries","Array","isArray","v","searchParams","append","set","response","fetch","headers","Authorization","next","revalidate","ok","Error","status","statusText","json","getComparisonTotals","timeRange","resp","totals","data","find","d","name","uniqueViewers","unique_viewers","viewCount","view_count","watchTimeMs","watch_time","getViewsOverall","getViewerExperienceScore","getViewsTimeseries","group_by","getWatchTimeTimeseries","getVideoBreakdown","limit","order_by","order_direction","String","getVideoBreakdownForRange","getCountryBreakdown","getVideoDetailBreakdowns","videoTitle","filter","countries","timeseries","Promise","all","map","c","country","field","views","total_watch_time","date","getVideoDashboardData","experience","comparison","watchTime","videos","overview","totalViews","total_views","totalWatchTimeMs","totalPlayingTimeMs","total_playing_time","viewerExperienceScore","globalExperienceScore","global_value","watchTimeSeries","totalMs","topVideos","title","playingTimeMs","getVideoThumbnails","db","contentResource","byId","byTitle","videoIds","length","rows","select","id","playbackId","sql","fields","as","where","join","idToPlayback","Map","r","result","i","Math","min","videoId","get","and","count","eq","sql","normalizeRespondentKey","row","respondentKey","userId","emailListSubscriberId","surveySessionId","getRowTimestamp","date","updatedAt","createdAt","Date","getTime","sortByNewest","rows","sort","a","b","createSurveyProvider","db","schema","contentResource","contentResourceResource","questionResponse","rangeToInterval","range","rangeWhere","column","sql","raw","fetchCanonicalRows","users","rawRows","select","responseId","id","surveyId","surveyTitle","surveySlug","questionId","questionText","questionType","answer","fields","userEmail","email","from","leftJoin","eq","where","latestByAnswer","Map","dedupeKey","current","get","set","Array","values","getSurveySummary","surveyCount","total","count","type","canonicalRows","respondentKeys","Set","map","totalSurveys","totalResponses","length","uniqueRespondents","size","avgResponsesPerSurvey","getSurveyList","responsesBySurvey","responses","respondents","add","surveys","questionCounts","resourceOfId","questionCount","innerJoin","and","resourceId","groupBy","questionCountMap","qc","s","counts","getSurveyResponsesByDay","grouped","toISOString","slice","entries","entryA","entryB","localeCompare","getSurveyQuestionBreakdown","limit","question","answers","entry","answerDistribution","getSurveyResponses","String"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
type TimeRange = '7:days' | '30:days' | '90:days';
|
|
2
|
+
/** Ranges available on the Top Videos table (independent from page-level range) */
|
|
3
|
+
type VideoTableRange = '24:hours' | '7:days' | '30:days' | '90:days';
|
|
4
|
+
interface MuxOverallResponse {
|
|
5
|
+
data: {
|
|
6
|
+
value: number;
|
|
7
|
+
total_watch_time: number;
|
|
8
|
+
total_playing_time: number;
|
|
9
|
+
total_views: number;
|
|
10
|
+
global_value: number | null;
|
|
11
|
+
};
|
|
12
|
+
timeframe: [number, number];
|
|
13
|
+
}
|
|
14
|
+
interface MuxTimeseriesResponse {
|
|
15
|
+
data: [string, number | null, number | null][];
|
|
16
|
+
timeframe: [number, number];
|
|
17
|
+
total_row_count: number;
|
|
18
|
+
}
|
|
19
|
+
interface MuxBreakdownItem {
|
|
20
|
+
views: number;
|
|
21
|
+
value: number;
|
|
22
|
+
total_watch_time: number;
|
|
23
|
+
total_playing_time: number;
|
|
24
|
+
negative_impact: number | null;
|
|
25
|
+
field: string;
|
|
26
|
+
}
|
|
27
|
+
interface MuxBreakdownResponse {
|
|
28
|
+
data: MuxBreakdownItem[];
|
|
29
|
+
timeframe: [number, number];
|
|
30
|
+
total_row_count: number;
|
|
31
|
+
}
|
|
32
|
+
interface VideoDashboardData {
|
|
33
|
+
overview: {
|
|
34
|
+
totalViews: number;
|
|
35
|
+
uniqueViewers: number;
|
|
36
|
+
totalWatchTimeMs: number;
|
|
37
|
+
totalPlayingTimeMs: number;
|
|
38
|
+
viewerExperienceScore: number;
|
|
39
|
+
globalExperienceScore: number | null;
|
|
40
|
+
};
|
|
41
|
+
watchTimeSeries: {
|
|
42
|
+
date: string;
|
|
43
|
+
watchTimeMs: number;
|
|
44
|
+
}[];
|
|
45
|
+
topVideos: {
|
|
46
|
+
title: string;
|
|
47
|
+
views: number;
|
|
48
|
+
watchTimeMs: number;
|
|
49
|
+
playingTimeMs: number;
|
|
50
|
+
}[];
|
|
51
|
+
countries: {
|
|
52
|
+
country: string;
|
|
53
|
+
views: number;
|
|
54
|
+
watchTimeMs: number;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
type VideoDetailBreakdowns = {
|
|
58
|
+
countries: {
|
|
59
|
+
country: string;
|
|
60
|
+
views: number;
|
|
61
|
+
watchTimeMs: number;
|
|
62
|
+
}[];
|
|
63
|
+
timeseries: {
|
|
64
|
+
date: string;
|
|
65
|
+
views: number;
|
|
66
|
+
}[];
|
|
67
|
+
};
|
|
68
|
+
interface MuxProviderConfig {
|
|
69
|
+
tokenId: string;
|
|
70
|
+
tokenSecret: string;
|
|
71
|
+
}
|
|
72
|
+
interface MuxDbDeps {
|
|
73
|
+
db: any;
|
|
74
|
+
contentResource: any;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Creates a Mux analytics provider with injected credentials.
|
|
78
|
+
* Optionally accepts db dependencies for thumbnail lookups that
|
|
79
|
+
* require querying ContentResource records.
|
|
80
|
+
*
|
|
81
|
+
* @param config - Mux Data API token configuration
|
|
82
|
+
* @param dbDeps - Optional drizzle db + contentResource table for thumbnail queries
|
|
83
|
+
*/
|
|
84
|
+
declare function createMuxProvider(config: MuxProviderConfig, dbDeps?: MuxDbDeps): {
|
|
85
|
+
getComparisonTotals: (timeRange?: TimeRange) => Promise<{
|
|
86
|
+
uniqueViewers: number;
|
|
87
|
+
viewCount: number;
|
|
88
|
+
watchTimeMs: number;
|
|
89
|
+
}>;
|
|
90
|
+
getViewsOverall: (timeRange?: TimeRange) => Promise<MuxOverallResponse>;
|
|
91
|
+
getViewerExperienceScore: (timeRange?: TimeRange) => Promise<MuxOverallResponse>;
|
|
92
|
+
getViewsTimeseries: (timeRange?: TimeRange) => Promise<MuxTimeseriesResponse>;
|
|
93
|
+
getWatchTimeTimeseries: (timeRange?: TimeRange) => Promise<MuxTimeseriesResponse>;
|
|
94
|
+
getVideoBreakdown: (timeRange?: TimeRange, limit?: number) => Promise<MuxBreakdownResponse>;
|
|
95
|
+
getVideoBreakdownForRange: (timeRange: VideoTableRange, limit?: number) => Promise<MuxBreakdownResponse>;
|
|
96
|
+
getCountryBreakdown: (timeRange?: TimeRange, limit?: number) => Promise<MuxBreakdownResponse>;
|
|
97
|
+
getVideoDetailBreakdowns: (videoTitle: string, timeRange?: TimeRange) => Promise<VideoDetailBreakdowns>;
|
|
98
|
+
getVideoDashboardData: (timeRange?: TimeRange) => Promise<VideoDashboardData>;
|
|
99
|
+
getVideoThumbnails: (timeRange?: TimeRange, limit?: number) => Promise<Record<string, string>>;
|
|
100
|
+
};
|
|
101
|
+
type MuxAnalyticsProvider = ReturnType<typeof createMuxProvider>;
|
|
102
|
+
|
|
103
|
+
export { type MuxAnalyticsProvider, type MuxBreakdownItem, type MuxBreakdownResponse, type MuxDbDeps, type MuxOverallResponse, type MuxProviderConfig, type MuxTimeseriesResponse, type TimeRange, type VideoDashboardData, type VideoDetailBreakdowns, type VideoTableRange, createMuxProvider };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/providers/mux.ts
|
|
5
|
+
import { sql } from "drizzle-orm";
|
|
6
|
+
function createMuxProvider(config, dbDeps) {
|
|
7
|
+
const MUX_DATA_BASE = "https://api.mux.com/data/v1";
|
|
8
|
+
function getAuthHeader() {
|
|
9
|
+
return `Basic ${Buffer.from(`${config.tokenId}:${config.tokenSecret}`).toString("base64")}`;
|
|
10
|
+
}
|
|
11
|
+
__name(getAuthHeader, "getAuthHeader");
|
|
12
|
+
async function muxDataFetch(path, params) {
|
|
13
|
+
const url = new URL(`${MUX_DATA_BASE}${path}`);
|
|
14
|
+
if (params) {
|
|
15
|
+
for (const [key, value] of Object.entries(params)) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
for (const v of value) {
|
|
18
|
+
url.searchParams.append(key, v);
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
url.searchParams.set(key, value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const response = await fetch(url.toString(), {
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: getAuthHeader(),
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
next: {
|
|
31
|
+
revalidate: 300
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Mux Data API error: ${response.status} ${response.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
__name(muxDataFetch, "muxDataFetch");
|
|
40
|
+
async function getComparisonTotals(timeRange = "30:days") {
|
|
41
|
+
const resp = await muxDataFetch("/metrics/comparison", {
|
|
42
|
+
"timeframe[]": timeRange
|
|
43
|
+
});
|
|
44
|
+
const totals = resp.data.find((d) => d.name === "totals");
|
|
45
|
+
return {
|
|
46
|
+
uniqueViewers: totals?.unique_viewers ?? 0,
|
|
47
|
+
viewCount: totals?.view_count ?? 0,
|
|
48
|
+
watchTimeMs: totals?.watch_time ?? 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
__name(getComparisonTotals, "getComparisonTotals");
|
|
52
|
+
async function getViewsOverall(timeRange = "30:days") {
|
|
53
|
+
return muxDataFetch("/metrics/views/overall", {
|
|
54
|
+
"timeframe[]": timeRange
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
__name(getViewsOverall, "getViewsOverall");
|
|
58
|
+
async function getViewerExperienceScore(timeRange = "30:days") {
|
|
59
|
+
return muxDataFetch("/metrics/viewer_experience_score/overall", {
|
|
60
|
+
"timeframe[]": timeRange
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
__name(getViewerExperienceScore, "getViewerExperienceScore");
|
|
64
|
+
async function getViewsTimeseries(timeRange = "30:days") {
|
|
65
|
+
return muxDataFetch("/metrics/views/timeseries", {
|
|
66
|
+
"timeframe[]": timeRange,
|
|
67
|
+
group_by: "day"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
__name(getViewsTimeseries, "getViewsTimeseries");
|
|
71
|
+
async function getWatchTimeTimeseries(timeRange = "30:days") {
|
|
72
|
+
return muxDataFetch("/metrics/playing_time/timeseries", {
|
|
73
|
+
"timeframe[]": timeRange,
|
|
74
|
+
group_by: "day"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
__name(getWatchTimeTimeseries, "getWatchTimeTimeseries");
|
|
78
|
+
async function getVideoBreakdown(timeRange = "30:days", limit = 25) {
|
|
79
|
+
return muxDataFetch("/metrics/views/breakdown", {
|
|
80
|
+
"timeframe[]": timeRange,
|
|
81
|
+
group_by: "video_title",
|
|
82
|
+
order_by: "views",
|
|
83
|
+
order_direction: "desc",
|
|
84
|
+
limit: String(limit)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
__name(getVideoBreakdown, "getVideoBreakdown");
|
|
88
|
+
async function getVideoBreakdownForRange(timeRange, limit = 50) {
|
|
89
|
+
return muxDataFetch("/metrics/views/breakdown", {
|
|
90
|
+
"timeframe[]": timeRange,
|
|
91
|
+
group_by: "video_title",
|
|
92
|
+
order_by: "views",
|
|
93
|
+
order_direction: "desc",
|
|
94
|
+
limit: String(limit)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
__name(getVideoBreakdownForRange, "getVideoBreakdownForRange");
|
|
98
|
+
async function getCountryBreakdown(timeRange = "30:days", limit = 10) {
|
|
99
|
+
return muxDataFetch("/metrics/views/breakdown", {
|
|
100
|
+
"timeframe[]": timeRange,
|
|
101
|
+
group_by: "country",
|
|
102
|
+
order_by: "views",
|
|
103
|
+
order_direction: "desc",
|
|
104
|
+
limit: String(limit)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
__name(getCountryBreakdown, "getCountryBreakdown");
|
|
108
|
+
async function getVideoDetailBreakdowns(videoTitle, timeRange = "30:days") {
|
|
109
|
+
const filter = `video_title:${videoTitle}`;
|
|
110
|
+
const [countries, timeseries] = await Promise.all([
|
|
111
|
+
muxDataFetch("/metrics/views/breakdown", {
|
|
112
|
+
"timeframe[]": timeRange,
|
|
113
|
+
group_by: "country",
|
|
114
|
+
order_by: "views",
|
|
115
|
+
order_direction: "desc",
|
|
116
|
+
limit: "8",
|
|
117
|
+
"filters[]": filter
|
|
118
|
+
}),
|
|
119
|
+
muxDataFetch("/metrics/views/timeseries", {
|
|
120
|
+
"timeframe[]": timeRange,
|
|
121
|
+
group_by: "day",
|
|
122
|
+
"filters[]": filter
|
|
123
|
+
})
|
|
124
|
+
]);
|
|
125
|
+
return {
|
|
126
|
+
countries: countries.data.map((c) => ({
|
|
127
|
+
country: c.field,
|
|
128
|
+
views: c.views,
|
|
129
|
+
watchTimeMs: c.total_watch_time
|
|
130
|
+
})),
|
|
131
|
+
timeseries: timeseries.data.map(([date, value]) => ({
|
|
132
|
+
date,
|
|
133
|
+
views: value ?? 0
|
|
134
|
+
}))
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
__name(getVideoDetailBreakdowns, "getVideoDetailBreakdowns");
|
|
138
|
+
async function getVideoDashboardData(timeRange = "30:days") {
|
|
139
|
+
const [views, experience, comparison, watchTime, videos, countries] = await Promise.all([
|
|
140
|
+
getViewsOverall(timeRange),
|
|
141
|
+
getViewerExperienceScore(timeRange),
|
|
142
|
+
getComparisonTotals(timeRange),
|
|
143
|
+
getWatchTimeTimeseries(timeRange),
|
|
144
|
+
getVideoBreakdown(timeRange, 50),
|
|
145
|
+
getCountryBreakdown(timeRange, 15)
|
|
146
|
+
]);
|
|
147
|
+
return {
|
|
148
|
+
overview: {
|
|
149
|
+
totalViews: views.data.total_views,
|
|
150
|
+
uniqueViewers: comparison.uniqueViewers,
|
|
151
|
+
totalWatchTimeMs: views.data.total_watch_time,
|
|
152
|
+
totalPlayingTimeMs: views.data.total_playing_time,
|
|
153
|
+
viewerExperienceScore: experience.data.value,
|
|
154
|
+
globalExperienceScore: experience.data.global_value
|
|
155
|
+
},
|
|
156
|
+
watchTimeSeries: watchTime.data.map(([date, totalMs]) => ({
|
|
157
|
+
date,
|
|
158
|
+
// Mux playing_time timeseries: value is total playing time in ms
|
|
159
|
+
watchTimeMs: totalMs ?? 0
|
|
160
|
+
})),
|
|
161
|
+
topVideos: videos.data.filter((v) => v.field !== "").map((v) => ({
|
|
162
|
+
title: v.field,
|
|
163
|
+
views: v.views,
|
|
164
|
+
watchTimeMs: v.total_watch_time,
|
|
165
|
+
playingTimeMs: v.total_playing_time
|
|
166
|
+
})),
|
|
167
|
+
countries: countries.data.map((c) => ({
|
|
168
|
+
country: c.field,
|
|
169
|
+
views: c.views,
|
|
170
|
+
watchTimeMs: c.total_watch_time
|
|
171
|
+
}))
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
__name(getVideoDashboardData, "getVideoDashboardData");
|
|
175
|
+
async function getVideoThumbnails(timeRange = "30:days", limit = 10) {
|
|
176
|
+
if (!dbDeps) {
|
|
177
|
+
throw new Error("getVideoThumbnails requires dbDeps (db + contentResource) to be provided to createMuxProvider");
|
|
178
|
+
}
|
|
179
|
+
const { db, contentResource } = dbDeps;
|
|
180
|
+
const [byId, byTitle] = await Promise.all([
|
|
181
|
+
muxDataFetch("/metrics/views/breakdown", {
|
|
182
|
+
"timeframe[]": timeRange,
|
|
183
|
+
group_by: "video_id",
|
|
184
|
+
order_by: "views",
|
|
185
|
+
order_direction: "desc",
|
|
186
|
+
limit: String(limit)
|
|
187
|
+
}),
|
|
188
|
+
muxDataFetch("/metrics/views/breakdown", {
|
|
189
|
+
"timeframe[]": timeRange,
|
|
190
|
+
group_by: "video_title",
|
|
191
|
+
order_by: "views",
|
|
192
|
+
order_direction: "desc",
|
|
193
|
+
limit: String(limit)
|
|
194
|
+
})
|
|
195
|
+
]);
|
|
196
|
+
const videoIds = byId.data.filter((v) => v.field && v.field !== "").map((v) => v.field);
|
|
197
|
+
if (videoIds.length === 0)
|
|
198
|
+
return {};
|
|
199
|
+
const rows = await db.select({
|
|
200
|
+
id: contentResource.id,
|
|
201
|
+
playbackId: sql`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.muxPlaybackId'))`.as("playbackId")
|
|
202
|
+
}).from(contentResource).where(sql`${contentResource.id} IN (${sql.join(videoIds.map((id) => sql`${id}`), sql`, `)})`);
|
|
203
|
+
const idToPlayback = /* @__PURE__ */ new Map();
|
|
204
|
+
for (const r of rows) {
|
|
205
|
+
if (r.playbackId && r.playbackId !== "null") {
|
|
206
|
+
idToPlayback.set(r.id, r.playbackId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const result = {};
|
|
210
|
+
for (let i = 0; i < Math.min(byId.data.length, byTitle.data.length); i++) {
|
|
211
|
+
const videoId = byId.data[i]?.field;
|
|
212
|
+
const title = byTitle.data[i]?.field;
|
|
213
|
+
if (!videoId || !title)
|
|
214
|
+
continue;
|
|
215
|
+
const playbackId = idToPlayback.get(videoId);
|
|
216
|
+
if (playbackId) {
|
|
217
|
+
result[title] = `https://image.mux.com/${playbackId}/thumbnail.jpg?width=240&height=135&fit_mode=smartcrop`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
__name(getVideoThumbnails, "getVideoThumbnails");
|
|
223
|
+
return {
|
|
224
|
+
getComparisonTotals,
|
|
225
|
+
getViewsOverall,
|
|
226
|
+
getViewerExperienceScore,
|
|
227
|
+
getViewsTimeseries,
|
|
228
|
+
getWatchTimeTimeseries,
|
|
229
|
+
getVideoBreakdown,
|
|
230
|
+
getVideoBreakdownForRange,
|
|
231
|
+
getCountryBreakdown,
|
|
232
|
+
getVideoDetailBreakdowns,
|
|
233
|
+
getVideoDashboardData,
|
|
234
|
+
getVideoThumbnails
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
__name(createMuxProvider, "createMuxProvider");
|
|
238
|
+
export {
|
|
239
|
+
createMuxProvider
|
|
240
|
+
};
|
|
241
|
+
//# sourceMappingURL=mux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/mux.ts"],"sourcesContent":["import { sql } from 'drizzle-orm'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type TimeRange = '7:days' | '30:days' | '90:days'\n\n/** Ranges available on the Top Videos table (independent from page-level range) */\nexport type VideoTableRange = '24:hours' | '7:days' | '30:days' | '90:days'\n\nexport interface MuxOverallResponse {\n\tdata: {\n\t\tvalue: number\n\t\ttotal_watch_time: number\n\t\ttotal_playing_time: number\n\t\ttotal_views: number\n\t\tglobal_value: number | null\n\t}\n\ttimeframe: [number, number]\n}\n\nexport interface MuxTimeseriesResponse {\n\tdata: [string, number | null, number | null][]\n\ttimeframe: [number, number]\n\ttotal_row_count: number\n}\n\nexport interface MuxBreakdownItem {\n\tviews: number\n\tvalue: number\n\ttotal_watch_time: number\n\ttotal_playing_time: number\n\tnegative_impact: number | null\n\tfield: string\n}\n\nexport interface MuxBreakdownResponse {\n\tdata: MuxBreakdownItem[]\n\ttimeframe: [number, number]\n\ttotal_row_count: number\n}\n\nexport interface VideoDashboardData {\n\toverview: {\n\t\ttotalViews: number\n\t\tuniqueViewers: number\n\t\ttotalWatchTimeMs: number\n\t\ttotalPlayingTimeMs: number\n\t\tviewerExperienceScore: number\n\t\tglobalExperienceScore: number | null\n\t}\n\twatchTimeSeries: {\n\t\tdate: string\n\t\twatchTimeMs: number\n\t}[]\n\ttopVideos: {\n\t\ttitle: string\n\t\tviews: number\n\t\twatchTimeMs: number\n\t\tplayingTimeMs: number\n\t}[]\n\tcountries: {\n\t\tcountry: string\n\t\tviews: number\n\t\twatchTimeMs: number\n\t}[]\n}\n\nexport type VideoDetailBreakdowns = {\n\tcountries: {\n\t\tcountry: string\n\t\tviews: number\n\t\twatchTimeMs: number\n\t}[]\n\ttimeseries: {\n\t\tdate: string\n\t\tviews: number\n\t}[]\n}\n\nexport interface MuxProviderConfig {\n\ttokenId: string\n\ttokenSecret: string\n}\n\nexport interface MuxDbDeps {\n\tdb: any\n\tcontentResource: any\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates a Mux analytics provider with injected credentials.\n * Optionally accepts db dependencies for thumbnail lookups that\n * require querying ContentResource records.\n *\n * @param config - Mux Data API token configuration\n * @param dbDeps - Optional drizzle db + contentResource table for thumbnail queries\n */\nexport function createMuxProvider(\n\tconfig: MuxProviderConfig,\n\tdbDeps?: MuxDbDeps,\n) {\n\tconst MUX_DATA_BASE = 'https://api.mux.com/data/v1'\n\n\tfunction getAuthHeader(): string {\n\t\treturn `Basic ${Buffer.from(\n\t\t\t`${config.tokenId}:${config.tokenSecret}`,\n\t\t).toString('base64')}`\n\t}\n\n\tasync function muxDataFetch<T>(\n\t\tpath: string,\n\t\tparams?: Record<string, string | string[]>,\n\t): Promise<T> {\n\t\tconst url = new URL(`${MUX_DATA_BASE}${path}`)\n\t\tif (params) {\n\t\t\tfor (const [key, value] of Object.entries(params)) {\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\tfor (const v of value) {\n\t\t\t\t\t\turl.searchParams.append(key, v)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\turl.searchParams.set(key, value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst response = await fetch(url.toString(), {\n\t\t\theaders: {\n\t\t\t\tAuthorization: getAuthHeader(),\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tnext: { revalidate: 300 }, // cache 5 minutes\n\t\t} as RequestInit)\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Mux Data API error: ${response.status} ${response.statusText}`,\n\t\t\t)\n\t\t}\n\n\t\treturn response.json() as Promise<T>\n\t}\n\n\t// ─── Comparison (unique viewers) ─────────────────────────────────────────\n\n\tinterface MuxComparisonItem {\n\t\tname: string\n\t\twatch_time?: number\n\t\tview_count?: number\n\t\tunique_viewers?: number\n\t\tstarted_views?: number\n\t\tended_views?: number\n\t\t[key: string]: unknown\n\t}\n\n\tinterface MuxComparisonResponse {\n\t\tdata: MuxComparisonItem[]\n\t\ttimeframe: [number, number]\n\t\ttotal_row_count: number | null\n\t}\n\n\tasync function getComparisonTotals(timeRange: TimeRange = '30:days') {\n\t\tconst resp = await muxDataFetch<MuxComparisonResponse>(\n\t\t\t'/metrics/comparison',\n\t\t\t{ 'timeframe[]': timeRange },\n\t\t)\n\t\tconst totals = resp.data.find((d) => d.name === 'totals')\n\t\treturn {\n\t\t\tuniqueViewers: totals?.unique_viewers ?? 0,\n\t\t\tviewCount: totals?.view_count ?? 0,\n\t\t\twatchTimeMs: totals?.watch_time ?? 0,\n\t\t}\n\t}\n\n\t// ─── API Functions ────────────────────────────────────────────────────────\n\n\tasync function getViewsOverall(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxOverallResponse>('/metrics/views/overall', {\n\t\t\t'timeframe[]': timeRange,\n\t\t})\n\t}\n\n\tasync function getViewerExperienceScore(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxOverallResponse>(\n\t\t\t'/metrics/viewer_experience_score/overall',\n\t\t\t{ 'timeframe[]': timeRange },\n\t\t)\n\t}\n\n\tasync function getViewsTimeseries(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxTimeseriesResponse>('/metrics/views/timeseries', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'day',\n\t\t})\n\t}\n\n\t/**\n\t * Fetch watch time (playing_time) timeseries grouped by day.\n\t * Mux returns [date, totalPlayingTimeMs, viewCount] tuples.\n\t * The value IS the total playing time in ms (not the average).\n\t */\n\tasync function getWatchTimeTimeseries(timeRange: TimeRange = '30:days') {\n\t\treturn muxDataFetch<MuxTimeseriesResponse>(\n\t\t\t'/metrics/playing_time/timeseries',\n\t\t\t{\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'day',\n\t\t\t},\n\t\t)\n\t}\n\n\tasync function getVideoBreakdown(\n\t\ttimeRange: TimeRange = '30:days',\n\t\tlimit: number = 25,\n\t) {\n\t\treturn muxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'video_title',\n\t\t\torder_by: 'views',\n\t\t\torder_direction: 'desc',\n\t\t\tlimit: String(limit),\n\t\t})\n\t}\n\n\t/**\n\t * Standalone video breakdown fetcher for the Top Videos table's\n\t * independent time-range tabs. Accepts VideoTableRange.\n\t */\n\tasync function getVideoBreakdownForRange(\n\t\ttimeRange: VideoTableRange,\n\t\tlimit: number = 50,\n\t) {\n\t\treturn muxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'video_title',\n\t\t\torder_by: 'views',\n\t\t\torder_direction: 'desc',\n\t\t\tlimit: String(limit),\n\t\t})\n\t}\n\n\tasync function getCountryBreakdown(\n\t\ttimeRange: TimeRange = '30:days',\n\t\tlimit: number = 10,\n\t) {\n\t\treturn muxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t'timeframe[]': timeRange,\n\t\t\tgroup_by: 'country',\n\t\t\torder_by: 'views',\n\t\t\torder_direction: 'desc',\n\t\t\tlimit: String(limit),\n\t\t})\n\t}\n\n\tasync function getVideoDetailBreakdowns(\n\t\tvideoTitle: string,\n\t\ttimeRange: TimeRange = '30:days',\n\t): Promise<VideoDetailBreakdowns> {\n\t\tconst filter = `video_title:${videoTitle}`\n\t\tconst [countries, timeseries] = await Promise.all([\n\t\t\tmuxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'country',\n\t\t\t\torder_by: 'views',\n\t\t\t\torder_direction: 'desc',\n\t\t\t\tlimit: '8',\n\t\t\t\t'filters[]': filter,\n\t\t\t}),\n\t\t\tmuxDataFetch<MuxTimeseriesResponse>('/metrics/views/timeseries', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'day',\n\t\t\t\t'filters[]': filter,\n\t\t\t}),\n\t\t])\n\n\t\treturn {\n\t\t\tcountries: countries.data.map((c) => ({\n\t\t\t\tcountry: c.field,\n\t\t\t\tviews: c.views,\n\t\t\t\twatchTimeMs: c.total_watch_time,\n\t\t\t})),\n\t\t\ttimeseries: timeseries.data.map(([date, value]) => ({\n\t\t\t\tdate,\n\t\t\t\tviews: value ?? 0,\n\t\t\t})),\n\t\t}\n\t}\n\n\t// ─── Aggregate fetcher ────────────────────────────────────────────────────\n\n\tasync function getVideoDashboardData(\n\t\ttimeRange: TimeRange = '30:days',\n\t): Promise<VideoDashboardData> {\n\t\tconst [views, experience, comparison, watchTime, videos, countries] =\n\t\t\tawait Promise.all([\n\t\t\t\tgetViewsOverall(timeRange),\n\t\t\t\tgetViewerExperienceScore(timeRange),\n\t\t\t\tgetComparisonTotals(timeRange),\n\t\t\t\tgetWatchTimeTimeseries(timeRange),\n\t\t\t\tgetVideoBreakdown(timeRange, 50),\n\t\t\t\tgetCountryBreakdown(timeRange, 15),\n\t\t\t])\n\n\t\treturn {\n\t\t\toverview: {\n\t\t\t\ttotalViews: views.data.total_views,\n\t\t\t\tuniqueViewers: comparison.uniqueViewers,\n\t\t\t\ttotalWatchTimeMs: views.data.total_watch_time,\n\t\t\t\ttotalPlayingTimeMs: views.data.total_playing_time,\n\t\t\t\tviewerExperienceScore: experience.data.value,\n\t\t\t\tglobalExperienceScore: experience.data.global_value,\n\t\t\t},\n\t\t\twatchTimeSeries: watchTime.data.map(([date, totalMs]) => ({\n\t\t\t\tdate,\n\t\t\t\t// Mux playing_time timeseries: value is total playing time in ms\n\t\t\t\twatchTimeMs: totalMs ?? 0,\n\t\t\t})),\n\t\t\ttopVideos: videos.data\n\t\t\t\t.filter((v) => v.field !== '')\n\t\t\t\t.map((v) => ({\n\t\t\t\t\ttitle: v.field,\n\t\t\t\t\tviews: v.views,\n\t\t\t\t\twatchTimeMs: v.total_watch_time,\n\t\t\t\t\tplayingTimeMs: v.total_playing_time,\n\t\t\t\t})),\n\t\t\tcountries: countries.data.map((c) => ({\n\t\t\t\tcountry: c.field,\n\t\t\t\tviews: c.views,\n\t\t\t\twatchTimeMs: c.total_watch_time,\n\t\t\t})),\n\t\t}\n\t}\n\n\t/**\n\t * Resolve video titles to Mux thumbnail URLs.\n\t * 1. Break down by video_id to get ContentResource IDs for top videos\n\t * 2. Look up playback IDs from ContentResource.fields.muxPlaybackId\n\t * 3. Build image.mux.com thumbnail URLs\n\t * Returns Record<title, thumbnailUrl>.\n\t * Requires dbDeps to be provided at factory construction time.\n\t */\n\tasync function getVideoThumbnails(\n\t\ttimeRange: TimeRange = '30:days',\n\t\tlimit: number = 10,\n\t): Promise<Record<string, string>> {\n\t\tif (!dbDeps) {\n\t\t\tthrow new Error(\n\t\t\t\t'getVideoThumbnails requires dbDeps (db + contentResource) ' +\n\t\t\t\t\t'to be provided to createMuxProvider',\n\t\t\t)\n\t\t}\n\n\t\tconst { db, contentResource } = dbDeps\n\n\t\t// Get breakdown by both video_id and video_title\n\t\tconst [byId, byTitle] = await Promise.all([\n\t\t\tmuxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'video_id',\n\t\t\t\torder_by: 'views',\n\t\t\t\torder_direction: 'desc',\n\t\t\t\tlimit: String(limit),\n\t\t\t}),\n\t\t\tmuxDataFetch<MuxBreakdownResponse>('/metrics/views/breakdown', {\n\t\t\t\t'timeframe[]': timeRange,\n\t\t\t\tgroup_by: 'video_title',\n\t\t\t\torder_by: 'views',\n\t\t\t\torder_direction: 'desc',\n\t\t\t\tlimit: String(limit),\n\t\t\t}),\n\t\t])\n\n\t\t// video_id breakdown gives ContentResource IDs — look up playback IDs\n\t\tconst videoIds = byId.data\n\t\t\t.filter((v) => v.field && v.field !== '')\n\t\t\t.map((v) => v.field)\n\n\t\tif (videoIds.length === 0) return {}\n\n\t\tconst rows = await db\n\t\t\t.select({\n\t\t\t\tid: contentResource.id,\n\t\t\t\tplaybackId:\n\t\t\t\t\tsql<string>`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.muxPlaybackId'))`.as(\n\t\t\t\t\t\t'playbackId',\n\t\t\t\t\t),\n\t\t\t})\n\t\t\t.from(contentResource)\n\t\t\t.where(\n\t\t\t\tsql`${contentResource.id} IN (${sql.join(\n\t\t\t\t\tvideoIds.map((id: string) => sql`${id}`),\n\t\t\t\t\tsql`, `,\n\t\t\t\t)})`,\n\t\t\t)\n\n\t\t// Map video_id → playbackId\n\t\tconst idToPlayback = new Map<string, string>()\n\t\tfor (const r of rows) {\n\t\t\tif (r.playbackId && r.playbackId !== 'null') {\n\t\t\t\tidToPlayback.set(r.id, r.playbackId)\n\t\t\t}\n\t\t}\n\n\t\t// Match by position: byId and byTitle are both sorted by views desc,\n\t\t// so position i in byId corresponds to position i in byTitle\n\t\tconst result: Record<string, string> = {}\n\t\tfor (let i = 0; i < Math.min(byId.data.length, byTitle.data.length); i++) {\n\t\t\tconst videoId = byId.data[i]?.field\n\t\t\tconst title = byTitle.data[i]?.field\n\t\t\tif (!videoId || !title) continue\n\t\t\tconst playbackId = idToPlayback.get(videoId)\n\t\t\tif (playbackId) {\n\t\t\t\tresult[title] =\n\t\t\t\t\t`https://image.mux.com/${playbackId}/thumbnail.jpg?width=240&height=135&fit_mode=smartcrop`\n\t\t\t}\n\t\t}\n\n\t\treturn result\n\t}\n\n\treturn {\n\t\tgetComparisonTotals,\n\t\tgetViewsOverall,\n\t\tgetViewerExperienceScore,\n\t\tgetViewsTimeseries,\n\t\tgetWatchTimeTimeseries,\n\t\tgetVideoBreakdown,\n\t\tgetVideoBreakdownForRange,\n\t\tgetCountryBreakdown,\n\t\tgetVideoDetailBreakdowns,\n\t\tgetVideoDashboardData,\n\t\tgetVideoThumbnails,\n\t}\n}\n\nexport type MuxAnalyticsProvider = ReturnType<typeof createMuxProvider>\n"],"mappings":"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","names":["sql","createMuxProvider","config","dbDeps","MUX_DATA_BASE","getAuthHeader","Buffer","from","tokenId","tokenSecret","toString","muxDataFetch","path","params","url","URL","key","value","Object","entries","Array","isArray","v","searchParams","append","set","response","fetch","headers","Authorization","next","revalidate","ok","Error","status","statusText","json","getComparisonTotals","timeRange","resp","totals","data","find","d","name","uniqueViewers","unique_viewers","viewCount","view_count","watchTimeMs","watch_time","getViewsOverall","getViewerExperienceScore","getViewsTimeseries","group_by","getWatchTimeTimeseries","getVideoBreakdown","limit","order_by","order_direction","String","getVideoBreakdownForRange","getCountryBreakdown","getVideoDetailBreakdowns","videoTitle","filter","countries","timeseries","Promise","all","map","c","country","field","views","total_watch_time","date","getVideoDashboardData","experience","comparison","watchTime","videos","overview","totalViews","total_views","totalWatchTimeMs","totalPlayingTimeMs","total_playing_time","viewerExperienceScore","globalExperienceScore","global_value","watchTimeSeries","totalMs","topVideos","title","playingTimeMs","getVideoThumbnails","db","contentResource","byId","byTitle","videoIds","length","rows","select","id","playbackId","sql","fields","as","where","join","idToPlayback","Map","r","result","i","Math","min","videoId","get"]}
|