@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,1239 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/providers/database.ts
|
|
5
|
+
import { and, count, desc, eq, gt, gte, inArray, lte, sql, sum } from "drizzle-orm";
|
|
6
|
+
function createDatabaseProvider(db, schema) {
|
|
7
|
+
const { purchases, products, users, coupon, resourceProgress, shortlink, shortlinkAttribution, shortlinkClick } = schema;
|
|
8
|
+
const PAID_STATUSES = [
|
|
9
|
+
"Valid",
|
|
10
|
+
"Restricted"
|
|
11
|
+
];
|
|
12
|
+
function paidPurchase() {
|
|
13
|
+
return inArray(purchases.status, [
|
|
14
|
+
...PAID_STATUSES
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
__name(paidPurchase, "paidPurchase");
|
|
18
|
+
function rangeToDate(range) {
|
|
19
|
+
if (range === "all")
|
|
20
|
+
return null;
|
|
21
|
+
const now = /* @__PURE__ */ new Date();
|
|
22
|
+
const hours = {
|
|
23
|
+
"24h": 24,
|
|
24
|
+
"7d": 7 * 24,
|
|
25
|
+
"30d": 30 * 24,
|
|
26
|
+
"90d": 90 * 24
|
|
27
|
+
};
|
|
28
|
+
return new Date(now.getTime() - (hours[range] ?? 30 * 24) * 60 * 60 * 1e3);
|
|
29
|
+
}
|
|
30
|
+
__name(rangeToDate, "rangeToDate");
|
|
31
|
+
async function getRevenueSummary(range = "30d") {
|
|
32
|
+
const since = rangeToDate(range);
|
|
33
|
+
const conditions = [
|
|
34
|
+
paidPurchase()
|
|
35
|
+
];
|
|
36
|
+
if (since)
|
|
37
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
38
|
+
const [totals] = await db.select({
|
|
39
|
+
totalRevenue: sum(purchases.totalAmount),
|
|
40
|
+
purchaseCount: count()
|
|
41
|
+
}).from(purchases).where(and(...conditions));
|
|
42
|
+
return {
|
|
43
|
+
totalRevenue: Number(totals?.totalRevenue ?? 0),
|
|
44
|
+
purchaseCount: totals?.purchaseCount ?? 0,
|
|
45
|
+
avgOrderValue: totals?.purchaseCount && totals.purchaseCount > 0 ? Number(totals.totalRevenue ?? 0) / totals.purchaseCount : 0
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
__name(getRevenueSummary, "getRevenueSummary");
|
|
49
|
+
async function getRevenueByDay(range = "30d") {
|
|
50
|
+
const since = rangeToDate(range);
|
|
51
|
+
const conditions = [
|
|
52
|
+
paidPurchase()
|
|
53
|
+
];
|
|
54
|
+
if (since)
|
|
55
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
56
|
+
const rows = await db.select({
|
|
57
|
+
date: sql`DATE(${purchases.createdAt})`.as("date"),
|
|
58
|
+
revenue: sum(purchases.totalAmount),
|
|
59
|
+
count: count()
|
|
60
|
+
}).from(purchases).where(and(...conditions)).groupBy(sql`DATE(${purchases.createdAt})`).orderBy(sql`DATE(${purchases.createdAt})`);
|
|
61
|
+
return rows.map((r) => ({
|
|
62
|
+
date: r.date,
|
|
63
|
+
revenue: Number(r.revenue ?? 0),
|
|
64
|
+
count: r.count
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
__name(getRevenueByDay, "getRevenueByDay");
|
|
68
|
+
async function getPreviousPeriodRevenueByDay(range = "30d") {
|
|
69
|
+
if (range === "all")
|
|
70
|
+
return [];
|
|
71
|
+
const hours = {
|
|
72
|
+
"24h": 24,
|
|
73
|
+
"7d": 7 * 24,
|
|
74
|
+
"30d": 30 * 24,
|
|
75
|
+
"90d": 90 * 24
|
|
76
|
+
};
|
|
77
|
+
const periodMs = (hours[range] ?? 30 * 24) * 60 * 60 * 1e3;
|
|
78
|
+
const now = /* @__PURE__ */ new Date();
|
|
79
|
+
const periodStart = new Date(now.getTime() - periodMs);
|
|
80
|
+
const prevStart = new Date(periodStart.getTime() - periodMs);
|
|
81
|
+
const rows = await db.select({
|
|
82
|
+
date: sql`DATE(${purchases.createdAt})`.as("date"),
|
|
83
|
+
revenue: sum(purchases.totalAmount),
|
|
84
|
+
count: count()
|
|
85
|
+
}).from(purchases).where(and(paidPurchase(), gte(purchases.createdAt, prevStart), lte(purchases.createdAt, periodStart))).groupBy(sql`DATE(${purchases.createdAt})`).orderBy(sql`DATE(${purchases.createdAt})`);
|
|
86
|
+
return rows.map((r) => ({
|
|
87
|
+
date: r.date,
|
|
88
|
+
revenue: Number(r.revenue ?? 0),
|
|
89
|
+
count: r.count
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
__name(getPreviousPeriodRevenueByDay, "getPreviousPeriodRevenueByDay");
|
|
93
|
+
async function getRevenueByProduct(range = "30d") {
|
|
94
|
+
const since = rangeToDate(range);
|
|
95
|
+
const conditions = [
|
|
96
|
+
paidPurchase()
|
|
97
|
+
];
|
|
98
|
+
if (since)
|
|
99
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
100
|
+
const rows = await db.select({
|
|
101
|
+
productId: purchases.productId,
|
|
102
|
+
productName: products.name,
|
|
103
|
+
revenue: sum(purchases.totalAmount),
|
|
104
|
+
count: count()
|
|
105
|
+
}).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(and(...conditions)).groupBy(purchases.productId, products.name).orderBy(desc(sum(purchases.totalAmount)));
|
|
106
|
+
return rows.map((r) => ({
|
|
107
|
+
productId: r.productId,
|
|
108
|
+
productName: r.productName ?? "(unknown)",
|
|
109
|
+
revenue: Number(r.revenue ?? 0),
|
|
110
|
+
count: r.count
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
__name(getRevenueByProduct, "getRevenueByProduct");
|
|
114
|
+
async function getRevenueByCountry(range = "30d") {
|
|
115
|
+
const since = rangeToDate(range);
|
|
116
|
+
const conditions = [
|
|
117
|
+
paidPurchase()
|
|
118
|
+
];
|
|
119
|
+
if (since)
|
|
120
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
121
|
+
const rows = await db.select({
|
|
122
|
+
country: purchases.country,
|
|
123
|
+
revenue: sum(purchases.totalAmount),
|
|
124
|
+
count: count()
|
|
125
|
+
}).from(purchases).where(and(...conditions)).groupBy(purchases.country).orderBy(desc(sum(purchases.totalAmount))).limit(20);
|
|
126
|
+
return rows.map((r) => ({
|
|
127
|
+
country: r.country ?? "(unknown)",
|
|
128
|
+
revenue: Number(r.revenue ?? 0),
|
|
129
|
+
count: r.count
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
__name(getRevenueByCountry, "getRevenueByCountry");
|
|
133
|
+
async function getRecentPurchases(limit = 20, filter = "all", range = "all") {
|
|
134
|
+
const since = rangeToDate(range);
|
|
135
|
+
const conditions = [
|
|
136
|
+
paidPurchase()
|
|
137
|
+
];
|
|
138
|
+
if (since)
|
|
139
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
140
|
+
if (filter === "team") {
|
|
141
|
+
conditions.push(sql`${purchases.bulkCouponId} IS NOT NULL`);
|
|
142
|
+
const rows2 = await db.select({
|
|
143
|
+
id: purchases.id,
|
|
144
|
+
createdAt: purchases.createdAt,
|
|
145
|
+
totalAmount: purchases.totalAmount,
|
|
146
|
+
productName: products.name,
|
|
147
|
+
productId: purchases.productId,
|
|
148
|
+
country: purchases.country,
|
|
149
|
+
couponId: purchases.couponId,
|
|
150
|
+
userId: purchases.userId,
|
|
151
|
+
userName: users.name,
|
|
152
|
+
userEmail: users.email,
|
|
153
|
+
organizationId: purchases.organizationId,
|
|
154
|
+
seats: coupon.maxUses
|
|
155
|
+
}).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).leftJoin(users, eq(purchases.userId, users.id)).leftJoin(coupon, eq(purchases.bulkCouponId, coupon.id)).where(and(...conditions, gt(coupon.maxUses, 1))).orderBy(desc(purchases.totalAmount)).limit(limit);
|
|
156
|
+
return rows2.map((r) => ({
|
|
157
|
+
id: r.id,
|
|
158
|
+
createdAt: r.createdAt,
|
|
159
|
+
totalAmount: Number(r.totalAmount),
|
|
160
|
+
productName: r.productName ?? "(unknown)",
|
|
161
|
+
productId: r.productId,
|
|
162
|
+
country: r.country,
|
|
163
|
+
couponId: r.couponId,
|
|
164
|
+
userName: r.userName ?? null,
|
|
165
|
+
userEmail: r.userEmail ?? null,
|
|
166
|
+
isTeam: true,
|
|
167
|
+
seats: r.seats ?? null
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
if (filter === "individual") {
|
|
171
|
+
conditions.push(sql`${purchases.bulkCouponId} IS NULL`);
|
|
172
|
+
}
|
|
173
|
+
const rows = await db.query.purchases.findMany({
|
|
174
|
+
where: and(...conditions),
|
|
175
|
+
orderBy: [
|
|
176
|
+
desc(purchases.totalAmount)
|
|
177
|
+
],
|
|
178
|
+
limit,
|
|
179
|
+
with: {
|
|
180
|
+
product: true,
|
|
181
|
+
user: true
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return rows.map((r) => ({
|
|
185
|
+
id: r.id,
|
|
186
|
+
createdAt: r.createdAt,
|
|
187
|
+
totalAmount: Number(r.totalAmount),
|
|
188
|
+
productName: r.product?.name ?? "(unknown)",
|
|
189
|
+
productId: r.productId,
|
|
190
|
+
country: r.country,
|
|
191
|
+
couponId: r.couponId,
|
|
192
|
+
userName: r.user?.name ?? null,
|
|
193
|
+
userEmail: r.user?.email ?? null,
|
|
194
|
+
isTeam: r.organizationId != null,
|
|
195
|
+
seats: null
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
__name(getRecentPurchases, "getRecentPurchases");
|
|
199
|
+
async function getAttributionSummary(range = "30d") {
|
|
200
|
+
const since = rangeToDate(range);
|
|
201
|
+
const conditions = [];
|
|
202
|
+
if (since)
|
|
203
|
+
conditions.push(gte(shortlinkAttribution.createdAt, since));
|
|
204
|
+
const rows = await db.select({
|
|
205
|
+
type: shortlinkAttribution.type,
|
|
206
|
+
count: count()
|
|
207
|
+
}).from(shortlinkAttribution).where(conditions.length > 0 ? and(...conditions) : void 0).groupBy(shortlinkAttribution.type);
|
|
208
|
+
return rows.map((r) => ({
|
|
209
|
+
type: r.type,
|
|
210
|
+
count: r.count
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
__name(getAttributionSummary, "getAttributionSummary");
|
|
214
|
+
async function getShortlinkPerformance(range = "30d") {
|
|
215
|
+
const since = rangeToDate(range);
|
|
216
|
+
const clickConditions = [];
|
|
217
|
+
if (since)
|
|
218
|
+
clickConditions.push(gte(shortlinkClick.timestamp, since));
|
|
219
|
+
const rows = await db.select({
|
|
220
|
+
shortlinkId: shortlinkClick.shortlinkId,
|
|
221
|
+
slug: shortlink.slug,
|
|
222
|
+
url: shortlink.url,
|
|
223
|
+
clicks: count()
|
|
224
|
+
}).from(shortlinkClick).innerJoin(shortlink, eq(shortlinkClick.shortlinkId, shortlink.id)).where(clickConditions.length > 0 ? and(...clickConditions) : void 0).groupBy(shortlinkClick.shortlinkId, shortlink.slug, shortlink.url).orderBy(desc(count())).limit(20);
|
|
225
|
+
const attrConditions = [];
|
|
226
|
+
if (since)
|
|
227
|
+
attrConditions.push(gte(shortlinkAttribution.createdAt, since));
|
|
228
|
+
const attrRows = await db.select({
|
|
229
|
+
shortlinkId: shortlinkAttribution.shortlinkId,
|
|
230
|
+
type: shortlinkAttribution.type,
|
|
231
|
+
count: count()
|
|
232
|
+
}).from(shortlinkAttribution).where(attrConditions.length > 0 ? and(...attrConditions) : void 0).groupBy(shortlinkAttribution.shortlinkId, shortlinkAttribution.type);
|
|
233
|
+
const attrMap = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const a of attrRows) {
|
|
235
|
+
const existing = attrMap.get(a.shortlinkId) ?? {
|
|
236
|
+
signups: 0,
|
|
237
|
+
purchases: 0
|
|
238
|
+
};
|
|
239
|
+
if (a.type === "signup")
|
|
240
|
+
existing.signups = a.count;
|
|
241
|
+
if (a.type === "purchase")
|
|
242
|
+
existing.purchases = a.count;
|
|
243
|
+
attrMap.set(a.shortlinkId, existing);
|
|
244
|
+
}
|
|
245
|
+
return rows.map((r) => {
|
|
246
|
+
const attr = attrMap.get(r.shortlinkId);
|
|
247
|
+
return {
|
|
248
|
+
shortlinkId: r.shortlinkId,
|
|
249
|
+
slug: r.slug,
|
|
250
|
+
url: r.url,
|
|
251
|
+
clicks: r.clicks,
|
|
252
|
+
signups: attr?.signups ?? 0,
|
|
253
|
+
purchases: attr?.purchases ?? 0
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
__name(getShortlinkPerformance, "getShortlinkPerformance");
|
|
258
|
+
async function getRevenueBySource(range = "30d") {
|
|
259
|
+
const since = rangeToDate(range);
|
|
260
|
+
const conditions = [
|
|
261
|
+
paidPurchase()
|
|
262
|
+
];
|
|
263
|
+
if (since)
|
|
264
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
265
|
+
const rows = await db.select({
|
|
266
|
+
source: sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`.as("source"),
|
|
267
|
+
medium: sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`.as("medium"),
|
|
268
|
+
campaign: sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`.as("campaign"),
|
|
269
|
+
revenue: sum(purchases.totalAmount),
|
|
270
|
+
count: count()
|
|
271
|
+
}).from(purchases).where(and(...conditions)).groupBy(sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource'))`, sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium'))`, sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign'))`).orderBy(desc(sum(purchases.totalAmount)));
|
|
272
|
+
return rows.map((r) => ({
|
|
273
|
+
source: r.source ?? null,
|
|
274
|
+
medium: r.medium ?? null,
|
|
275
|
+
campaign: r.campaign ?? null,
|
|
276
|
+
revenue: Number(r.revenue ?? 0),
|
|
277
|
+
count: r.count
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
__name(getRevenueBySource, "getRevenueBySource");
|
|
281
|
+
async function getConversionFunnel(range = "30d") {
|
|
282
|
+
const since = rangeToDate(range);
|
|
283
|
+
const userConditions = since ? [
|
|
284
|
+
gte(users.createdAt, since)
|
|
285
|
+
] : [];
|
|
286
|
+
const purchaseConditions = [
|
|
287
|
+
paidPurchase()
|
|
288
|
+
];
|
|
289
|
+
if (since)
|
|
290
|
+
purchaseConditions.push(gte(purchases.createdAt, since));
|
|
291
|
+
const [userCount] = await db.select({
|
|
292
|
+
total: count()
|
|
293
|
+
}).from(users).where(userConditions.length > 0 ? and(...userConditions) : void 0);
|
|
294
|
+
const [purchaseCount] = await db.select({
|
|
295
|
+
total: count()
|
|
296
|
+
}).from(purchases).where(and(...purchaseConditions));
|
|
297
|
+
const [attributedCount] = await db.select({
|
|
298
|
+
total: count()
|
|
299
|
+
}).from(purchases).where(and(...purchaseConditions, sql`(
|
|
300
|
+
JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
301
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
302
|
+
)`));
|
|
303
|
+
const totalSignups = userCount?.total ?? 0;
|
|
304
|
+
const totalPurchases = purchaseCount?.total ?? 0;
|
|
305
|
+
const attributedPurchases = attributedCount?.total ?? 0;
|
|
306
|
+
return {
|
|
307
|
+
totalSignups,
|
|
308
|
+
totalPurchases,
|
|
309
|
+
attributedPurchases,
|
|
310
|
+
conversionRate: totalSignups > 0 ? totalPurchases / totalSignups : 0,
|
|
311
|
+
attributionCoverage: totalPurchases > 0 ? attributedPurchases / totalPurchases : 0
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
__name(getConversionFunnel, "getConversionFunnel");
|
|
315
|
+
async function getAttributedRevenueSummary(range = "30d") {
|
|
316
|
+
const since = rangeToDate(range);
|
|
317
|
+
const conditions = [
|
|
318
|
+
paidPurchase()
|
|
319
|
+
];
|
|
320
|
+
if (since)
|
|
321
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
322
|
+
const [totals] = await db.select({
|
|
323
|
+
total: sum(purchases.totalAmount),
|
|
324
|
+
count: count()
|
|
325
|
+
}).from(purchases).where(and(...conditions));
|
|
326
|
+
const [attributed] = await db.select({
|
|
327
|
+
total: sum(purchases.totalAmount)
|
|
328
|
+
}).from(purchases).where(and(...conditions, sql`(
|
|
329
|
+
JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
330
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
331
|
+
)`));
|
|
332
|
+
const totalRevenue = Number(totals?.total ?? 0);
|
|
333
|
+
const attributedRevenue = Number(attributed?.total ?? 0);
|
|
334
|
+
const unattributedRevenue = totalRevenue - attributedRevenue;
|
|
335
|
+
const totalPurchases = totals?.count ?? 0;
|
|
336
|
+
return {
|
|
337
|
+
totalRevenue,
|
|
338
|
+
attributedRevenue,
|
|
339
|
+
unattributedRevenue,
|
|
340
|
+
attributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,
|
|
341
|
+
totalPurchases
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
__name(getAttributedRevenueSummary, "getAttributedRevenueSummary");
|
|
345
|
+
async function getContentPurchaseCorrelation(range = "30d", limit = 20) {
|
|
346
|
+
const since = rangeToDate(range);
|
|
347
|
+
const purchaseConditions = [
|
|
348
|
+
paidPurchase()
|
|
349
|
+
];
|
|
350
|
+
if (since)
|
|
351
|
+
purchaseConditions.push(gte(purchases.createdAt, since));
|
|
352
|
+
const purchaserRows = await db.selectDistinct({
|
|
353
|
+
userId: purchases.userId
|
|
354
|
+
}).from(purchases).where(and(...purchaseConditions));
|
|
355
|
+
const purchaserIds = purchaserRows.map((r) => r.userId).filter((id) => id !== null);
|
|
356
|
+
if (purchaserIds.length === 0)
|
|
357
|
+
return [];
|
|
358
|
+
const rows = await db.select({
|
|
359
|
+
resourceId: resourceProgress.resourceId,
|
|
360
|
+
purchaserCount: count()
|
|
361
|
+
}).from(resourceProgress).where(sql`${resourceProgress.userId} IN (${sql.join(purchaserIds.map((id) => sql`${id}`), sql`, `)})`).groupBy(resourceProgress.resourceId).orderBy(desc(count())).limit(limit);
|
|
362
|
+
return rows.map((r) => ({
|
|
363
|
+
resourceId: r.resourceId,
|
|
364
|
+
purchaserCount: r.purchaserCount
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
__name(getContentPurchaseCorrelation, "getContentPurchaseCorrelation");
|
|
368
|
+
async function traceAttribution(opts) {
|
|
369
|
+
const events = [];
|
|
370
|
+
let userId = null;
|
|
371
|
+
let userEmail = opts.email ?? null;
|
|
372
|
+
let userRecord = null;
|
|
373
|
+
if (opts.purchaseId) {
|
|
374
|
+
const [purchase] = await db.select({
|
|
375
|
+
userId: purchases.userId,
|
|
376
|
+
email: users.email
|
|
377
|
+
}).from(purchases).leftJoin(users, eq(purchases.userId, users.id)).where(eq(purchases.id, opts.purchaseId)).limit(1);
|
|
378
|
+
userId = purchase?.userId ?? null;
|
|
379
|
+
userEmail = purchase?.email ?? userEmail;
|
|
380
|
+
}
|
|
381
|
+
if (userEmail && !userId) {
|
|
382
|
+
const [u] = await db.select({
|
|
383
|
+
id: users.id
|
|
384
|
+
}).from(users).where(eq(users.email, userEmail)).limit(1);
|
|
385
|
+
userId = u?.id ?? null;
|
|
386
|
+
}
|
|
387
|
+
if (userId) {
|
|
388
|
+
const [u] = await db.select({
|
|
389
|
+
id: users.id,
|
|
390
|
+
email: users.email,
|
|
391
|
+
name: users.name,
|
|
392
|
+
createdAt: users.createdAt
|
|
393
|
+
}).from(users).where(eq(users.id, userId)).limit(1);
|
|
394
|
+
userRecord = u ? {
|
|
395
|
+
id: u.id,
|
|
396
|
+
email: u.email,
|
|
397
|
+
name: u.name ?? null,
|
|
398
|
+
createdAt: u.createdAt
|
|
399
|
+
} : null;
|
|
400
|
+
}
|
|
401
|
+
const attrConditions = [];
|
|
402
|
+
if (userId)
|
|
403
|
+
attrConditions.push(eq(shortlinkAttribution.userId, userId));
|
|
404
|
+
if (userEmail)
|
|
405
|
+
attrConditions.push(eq(shortlinkAttribution.email, userEmail));
|
|
406
|
+
if (attrConditions.length > 0) {
|
|
407
|
+
const attrs = await db.select({
|
|
408
|
+
type: shortlinkAttribution.type,
|
|
409
|
+
createdAt: shortlinkAttribution.createdAt,
|
|
410
|
+
metadata: shortlinkAttribution.metadata,
|
|
411
|
+
shortlinkId: shortlinkAttribution.shortlinkId,
|
|
412
|
+
slug: shortlink.slug,
|
|
413
|
+
url: shortlink.url
|
|
414
|
+
}).from(shortlinkAttribution).leftJoin(shortlink, eq(shortlinkAttribution.shortlinkId, shortlink.id)).where(sql`(${sql.join(attrConditions, sql` OR `)})`).orderBy(shortlinkAttribution.createdAt);
|
|
415
|
+
for (const attr of attrs) {
|
|
416
|
+
const clicks = await db.select({
|
|
417
|
+
timestamp: shortlinkClick.timestamp,
|
|
418
|
+
referrer: shortlinkClick.referrer,
|
|
419
|
+
country: shortlinkClick.country,
|
|
420
|
+
device: shortlinkClick.device
|
|
421
|
+
}).from(shortlinkClick).where(and(eq(shortlinkClick.shortlinkId, attr.shortlinkId), lte(shortlinkClick.timestamp, attr.createdAt))).orderBy(desc(shortlinkClick.timestamp)).limit(3);
|
|
422
|
+
for (const click of clicks) {
|
|
423
|
+
events.push({
|
|
424
|
+
type: "click",
|
|
425
|
+
timestamp: click.timestamp,
|
|
426
|
+
detail: {
|
|
427
|
+
shortlink: `/s/${attr.slug}`,
|
|
428
|
+
destination: attr.url,
|
|
429
|
+
referrer: click.referrer,
|
|
430
|
+
country: click.country,
|
|
431
|
+
device: click.device
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
events.push({
|
|
436
|
+
type: attr.type === "purchase" ? "purchase" : "signup",
|
|
437
|
+
timestamp: attr.createdAt,
|
|
438
|
+
detail: {
|
|
439
|
+
shortlink: `/s/${attr.slug}`,
|
|
440
|
+
metadata: attr.metadata ? JSON.parse(attr.metadata) : null
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (userId) {
|
|
446
|
+
const progress = await db.select({
|
|
447
|
+
resourceId: resourceProgress.resourceId,
|
|
448
|
+
completedAt: resourceProgress.completedAt,
|
|
449
|
+
createdAt: resourceProgress.createdAt
|
|
450
|
+
}).from(resourceProgress).where(eq(resourceProgress.userId, userId)).orderBy(resourceProgress.createdAt).limit(20);
|
|
451
|
+
for (const p of progress) {
|
|
452
|
+
events.push({
|
|
453
|
+
type: "progress",
|
|
454
|
+
timestamp: p.completedAt ?? p.createdAt,
|
|
455
|
+
detail: {
|
|
456
|
+
resourceId: p.resourceId
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const purchaseConditions = [
|
|
462
|
+
paidPurchase()
|
|
463
|
+
];
|
|
464
|
+
if (opts.purchaseId) {
|
|
465
|
+
purchaseConditions.push(eq(purchases.id, opts.purchaseId));
|
|
466
|
+
} else if (userId) {
|
|
467
|
+
purchaseConditions.push(eq(purchases.userId, userId));
|
|
468
|
+
} else {
|
|
469
|
+
return {
|
|
470
|
+
user: userRecord,
|
|
471
|
+
events: [],
|
|
472
|
+
purchases: []
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const purchaseRows = await db.select({
|
|
476
|
+
id: purchases.id,
|
|
477
|
+
totalAmount: purchases.totalAmount,
|
|
478
|
+
productName: products.name,
|
|
479
|
+
createdAt: purchases.createdAt,
|
|
480
|
+
country: purchases.country,
|
|
481
|
+
fields: purchases.fields
|
|
482
|
+
}).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(and(...purchaseConditions)).orderBy(purchases.createdAt);
|
|
483
|
+
const purchaseResults = purchaseRows.map((p) => {
|
|
484
|
+
const fields = p.fields ?? {};
|
|
485
|
+
events.push({
|
|
486
|
+
type: "purchase",
|
|
487
|
+
timestamp: p.createdAt,
|
|
488
|
+
detail: {
|
|
489
|
+
purchaseId: p.id,
|
|
490
|
+
amount: Number(p.totalAmount),
|
|
491
|
+
product: p.productName
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
id: p.id,
|
|
496
|
+
totalAmount: Number(p.totalAmount),
|
|
497
|
+
productName: p.productName ?? "Unknown",
|
|
498
|
+
createdAt: p.createdAt,
|
|
499
|
+
country: p.country,
|
|
500
|
+
utmSource: fields.utmSource ?? null,
|
|
501
|
+
utmMedium: fields.utmMedium ?? null,
|
|
502
|
+
utmCampaign: fields.utmCampaign ?? null
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
506
|
+
return {
|
|
507
|
+
user: userRecord,
|
|
508
|
+
events,
|
|
509
|
+
purchases: purchaseResults
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
__name(traceAttribution, "traceAttribution");
|
|
513
|
+
return {
|
|
514
|
+
getRevenueSummary,
|
|
515
|
+
getRevenueByDay,
|
|
516
|
+
getPreviousPeriodRevenueByDay,
|
|
517
|
+
getRevenueByProduct,
|
|
518
|
+
getRevenueByCountry,
|
|
519
|
+
getRecentPurchases,
|
|
520
|
+
getAttributionSummary,
|
|
521
|
+
getShortlinkPerformance,
|
|
522
|
+
getRevenueBySource,
|
|
523
|
+
getConversionFunnel,
|
|
524
|
+
getAttributedRevenueSummary,
|
|
525
|
+
getContentPurchaseCorrelation,
|
|
526
|
+
traceAttribution
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
__name(createDatabaseProvider, "createDatabaseProvider");
|
|
530
|
+
|
|
531
|
+
// src/providers/ga4.ts
|
|
532
|
+
import { BetaAnalyticsDataClient } from "@google-analytics/data";
|
|
533
|
+
function createGA4Provider(config) {
|
|
534
|
+
const { propertyId, clientEmail, privateKey } = config;
|
|
535
|
+
let _client = null;
|
|
536
|
+
function getClient() {
|
|
537
|
+
if (!_client) {
|
|
538
|
+
_client = new BetaAnalyticsDataClient({
|
|
539
|
+
credentials: {
|
|
540
|
+
client_email: clientEmail,
|
|
541
|
+
private_key: privateKey.replace(/\\n/g, "\n")
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return _client;
|
|
546
|
+
}
|
|
547
|
+
__name(getClient, "getClient");
|
|
548
|
+
function rangeToDateRange(range) {
|
|
549
|
+
const map = {
|
|
550
|
+
"24h": "1daysAgo",
|
|
551
|
+
"7d": "7daysAgo",
|
|
552
|
+
"30d": "30daysAgo",
|
|
553
|
+
"90d": "90daysAgo"
|
|
554
|
+
};
|
|
555
|
+
return {
|
|
556
|
+
startDate: map[range],
|
|
557
|
+
endDate: "today"
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
__name(rangeToDateRange, "rangeToDateRange");
|
|
561
|
+
async function getTrafficOverview(range = "30d") {
|
|
562
|
+
const client = getClient();
|
|
563
|
+
const [response] = await client.runReport({
|
|
564
|
+
property: `properties/${propertyId}`,
|
|
565
|
+
dateRanges: [
|
|
566
|
+
rangeToDateRange(range)
|
|
567
|
+
],
|
|
568
|
+
metrics: [
|
|
569
|
+
{
|
|
570
|
+
name: "sessions"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: "totalUsers"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "newUsers"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "screenPageViews"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: "averageSessionDuration"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "bounceRate"
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
});
|
|
589
|
+
const row = response?.rows?.[0];
|
|
590
|
+
if (!row) {
|
|
591
|
+
return {
|
|
592
|
+
sessions: 0,
|
|
593
|
+
totalUsers: 0,
|
|
594
|
+
newUsers: 0,
|
|
595
|
+
pageviews: 0,
|
|
596
|
+
avgSessionDuration: 0,
|
|
597
|
+
bounceRate: 0
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
sessions: Number(row.metricValues?.[0]?.value ?? 0),
|
|
602
|
+
totalUsers: Number(row.metricValues?.[1]?.value ?? 0),
|
|
603
|
+
newUsers: Number(row.metricValues?.[2]?.value ?? 0),
|
|
604
|
+
pageviews: Number(row.metricValues?.[3]?.value ?? 0),
|
|
605
|
+
avgSessionDuration: Number(row.metricValues?.[4]?.value ?? 0),
|
|
606
|
+
bounceRate: Number(row.metricValues?.[5]?.value ?? 0)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
__name(getTrafficOverview, "getTrafficOverview");
|
|
610
|
+
async function getTopPages(range = "30d", limit = 20) {
|
|
611
|
+
const client = getClient();
|
|
612
|
+
const [response] = await client.runReport({
|
|
613
|
+
property: `properties/${propertyId}`,
|
|
614
|
+
dateRanges: [
|
|
615
|
+
rangeToDateRange(range)
|
|
616
|
+
],
|
|
617
|
+
dimensions: [
|
|
618
|
+
{
|
|
619
|
+
name: "pagePath"
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
metrics: [
|
|
623
|
+
{
|
|
624
|
+
name: "screenPageViews"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: "totalUsers"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "averageSessionDuration"
|
|
631
|
+
}
|
|
632
|
+
],
|
|
633
|
+
orderBys: [
|
|
634
|
+
{
|
|
635
|
+
metric: {
|
|
636
|
+
metricName: "screenPageViews"
|
|
637
|
+
},
|
|
638
|
+
desc: true
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
limit
|
|
642
|
+
});
|
|
643
|
+
return response?.rows?.map((row) => ({
|
|
644
|
+
path: row.dimensionValues?.[0]?.value ?? "",
|
|
645
|
+
pageviews: Number(row.metricValues?.[0]?.value ?? 0),
|
|
646
|
+
users: Number(row.metricValues?.[1]?.value ?? 0),
|
|
647
|
+
avgDuration: Number(row.metricValues?.[2]?.value ?? 0)
|
|
648
|
+
})) ?? [];
|
|
649
|
+
}
|
|
650
|
+
__name(getTopPages, "getTopPages");
|
|
651
|
+
async function getTrafficSources(range = "30d", limit = 15) {
|
|
652
|
+
const client = getClient();
|
|
653
|
+
const [response] = await client.runReport({
|
|
654
|
+
property: `properties/${propertyId}`,
|
|
655
|
+
dateRanges: [
|
|
656
|
+
rangeToDateRange(range)
|
|
657
|
+
],
|
|
658
|
+
dimensions: [
|
|
659
|
+
{
|
|
660
|
+
name: "sessionSource"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "sessionMedium"
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
metrics: [
|
|
667
|
+
{
|
|
668
|
+
name: "sessions"
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "totalUsers"
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
orderBys: [
|
|
675
|
+
{
|
|
676
|
+
metric: {
|
|
677
|
+
metricName: "sessions"
|
|
678
|
+
},
|
|
679
|
+
desc: true
|
|
680
|
+
}
|
|
681
|
+
],
|
|
682
|
+
limit
|
|
683
|
+
});
|
|
684
|
+
return response?.rows?.map((row) => ({
|
|
685
|
+
source: row.dimensionValues?.[0]?.value ?? "(direct)",
|
|
686
|
+
medium: row.dimensionValues?.[1]?.value ?? "(none)",
|
|
687
|
+
sessions: Number(row.metricValues?.[0]?.value ?? 0),
|
|
688
|
+
users: Number(row.metricValues?.[1]?.value ?? 0)
|
|
689
|
+
})) ?? [];
|
|
690
|
+
}
|
|
691
|
+
__name(getTrafficSources, "getTrafficSources");
|
|
692
|
+
async function getSessionsByDay(range = "30d") {
|
|
693
|
+
const client = getClient();
|
|
694
|
+
const [response] = await client.runReport({
|
|
695
|
+
property: `properties/${propertyId}`,
|
|
696
|
+
dateRanges: [
|
|
697
|
+
rangeToDateRange(range)
|
|
698
|
+
],
|
|
699
|
+
dimensions: [
|
|
700
|
+
{
|
|
701
|
+
name: "date"
|
|
702
|
+
}
|
|
703
|
+
],
|
|
704
|
+
metrics: [
|
|
705
|
+
{
|
|
706
|
+
name: "sessions"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "totalUsers"
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
name: "screenPageViews"
|
|
713
|
+
}
|
|
714
|
+
],
|
|
715
|
+
orderBys: [
|
|
716
|
+
{
|
|
717
|
+
dimension: {
|
|
718
|
+
dimensionName: "date"
|
|
719
|
+
},
|
|
720
|
+
desc: false
|
|
721
|
+
}
|
|
722
|
+
]
|
|
723
|
+
});
|
|
724
|
+
return response?.rows?.map((row) => {
|
|
725
|
+
const raw = row.dimensionValues?.[0]?.value ?? "";
|
|
726
|
+
const date = `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`;
|
|
727
|
+
return {
|
|
728
|
+
date,
|
|
729
|
+
sessions: Number(row.metricValues?.[0]?.value ?? 0),
|
|
730
|
+
users: Number(row.metricValues?.[1]?.value ?? 0),
|
|
731
|
+
pageviews: Number(row.metricValues?.[2]?.value ?? 0)
|
|
732
|
+
};
|
|
733
|
+
}) ?? [];
|
|
734
|
+
}
|
|
735
|
+
__name(getSessionsByDay, "getSessionsByDay");
|
|
736
|
+
return {
|
|
737
|
+
getTrafficOverview,
|
|
738
|
+
getTopPages,
|
|
739
|
+
getTrafficSources,
|
|
740
|
+
getSessionsByDay
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
__name(createGA4Provider, "createGA4Provider");
|
|
744
|
+
|
|
745
|
+
// src/providers/derived.ts
|
|
746
|
+
function createDerivedProvider(deps) {
|
|
747
|
+
const { database, ga4 } = deps;
|
|
748
|
+
function toGA4Range(range) {
|
|
749
|
+
if (range === "all")
|
|
750
|
+
return "90d";
|
|
751
|
+
return range;
|
|
752
|
+
}
|
|
753
|
+
__name(toGA4Range, "toGA4Range");
|
|
754
|
+
async function getTrafficRevenueCorrelation(range) {
|
|
755
|
+
const [traffic, revenue] = await Promise.all([
|
|
756
|
+
ga4.getSessionsByDay(toGA4Range(range)),
|
|
757
|
+
database.getRevenueByDay(range)
|
|
758
|
+
]);
|
|
759
|
+
return {
|
|
760
|
+
traffic,
|
|
761
|
+
revenue
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
__name(getTrafficRevenueCorrelation, "getTrafficRevenueCorrelation");
|
|
765
|
+
return {
|
|
766
|
+
getTrafficRevenueCorrelation
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
__name(createDerivedProvider, "createDerivedProvider");
|
|
770
|
+
|
|
771
|
+
// src/providers/mux.ts
|
|
772
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
773
|
+
function createMuxProvider(config, dbDeps) {
|
|
774
|
+
const MUX_DATA_BASE = "https://api.mux.com/data/v1";
|
|
775
|
+
function getAuthHeader() {
|
|
776
|
+
return `Basic ${Buffer.from(`${config.tokenId}:${config.tokenSecret}`).toString("base64")}`;
|
|
777
|
+
}
|
|
778
|
+
__name(getAuthHeader, "getAuthHeader");
|
|
779
|
+
async function muxDataFetch(path, params) {
|
|
780
|
+
const url = new URL(`${MUX_DATA_BASE}${path}`);
|
|
781
|
+
if (params) {
|
|
782
|
+
for (const [key, value] of Object.entries(params)) {
|
|
783
|
+
if (Array.isArray(value)) {
|
|
784
|
+
for (const v of value) {
|
|
785
|
+
url.searchParams.append(key, v);
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
url.searchParams.set(key, value);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const response = await fetch(url.toString(), {
|
|
793
|
+
headers: {
|
|
794
|
+
Authorization: getAuthHeader(),
|
|
795
|
+
"Content-Type": "application/json"
|
|
796
|
+
},
|
|
797
|
+
next: {
|
|
798
|
+
revalidate: 300
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
if (!response.ok) {
|
|
802
|
+
throw new Error(`Mux Data API error: ${response.status} ${response.statusText}`);
|
|
803
|
+
}
|
|
804
|
+
return response.json();
|
|
805
|
+
}
|
|
806
|
+
__name(muxDataFetch, "muxDataFetch");
|
|
807
|
+
async function getComparisonTotals(timeRange = "30:days") {
|
|
808
|
+
const resp = await muxDataFetch("/metrics/comparison", {
|
|
809
|
+
"timeframe[]": timeRange
|
|
810
|
+
});
|
|
811
|
+
const totals = resp.data.find((d) => d.name === "totals");
|
|
812
|
+
return {
|
|
813
|
+
uniqueViewers: totals?.unique_viewers ?? 0,
|
|
814
|
+
viewCount: totals?.view_count ?? 0,
|
|
815
|
+
watchTimeMs: totals?.watch_time ?? 0
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
__name(getComparisonTotals, "getComparisonTotals");
|
|
819
|
+
async function getViewsOverall(timeRange = "30:days") {
|
|
820
|
+
return muxDataFetch("/metrics/views/overall", {
|
|
821
|
+
"timeframe[]": timeRange
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
__name(getViewsOverall, "getViewsOverall");
|
|
825
|
+
async function getViewerExperienceScore(timeRange = "30:days") {
|
|
826
|
+
return muxDataFetch("/metrics/viewer_experience_score/overall", {
|
|
827
|
+
"timeframe[]": timeRange
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
__name(getViewerExperienceScore, "getViewerExperienceScore");
|
|
831
|
+
async function getViewsTimeseries(timeRange = "30:days") {
|
|
832
|
+
return muxDataFetch("/metrics/views/timeseries", {
|
|
833
|
+
"timeframe[]": timeRange,
|
|
834
|
+
group_by: "day"
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
__name(getViewsTimeseries, "getViewsTimeseries");
|
|
838
|
+
async function getWatchTimeTimeseries(timeRange = "30:days") {
|
|
839
|
+
return muxDataFetch("/metrics/playing_time/timeseries", {
|
|
840
|
+
"timeframe[]": timeRange,
|
|
841
|
+
group_by: "day"
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
__name(getWatchTimeTimeseries, "getWatchTimeTimeseries");
|
|
845
|
+
async function getVideoBreakdown(timeRange = "30:days", limit = 25) {
|
|
846
|
+
return muxDataFetch("/metrics/views/breakdown", {
|
|
847
|
+
"timeframe[]": timeRange,
|
|
848
|
+
group_by: "video_title",
|
|
849
|
+
order_by: "views",
|
|
850
|
+
order_direction: "desc",
|
|
851
|
+
limit: String(limit)
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
__name(getVideoBreakdown, "getVideoBreakdown");
|
|
855
|
+
async function getVideoBreakdownForRange(timeRange, limit = 50) {
|
|
856
|
+
return muxDataFetch("/metrics/views/breakdown", {
|
|
857
|
+
"timeframe[]": timeRange,
|
|
858
|
+
group_by: "video_title",
|
|
859
|
+
order_by: "views",
|
|
860
|
+
order_direction: "desc",
|
|
861
|
+
limit: String(limit)
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
__name(getVideoBreakdownForRange, "getVideoBreakdownForRange");
|
|
865
|
+
async function getCountryBreakdown(timeRange = "30:days", limit = 10) {
|
|
866
|
+
return muxDataFetch("/metrics/views/breakdown", {
|
|
867
|
+
"timeframe[]": timeRange,
|
|
868
|
+
group_by: "country",
|
|
869
|
+
order_by: "views",
|
|
870
|
+
order_direction: "desc",
|
|
871
|
+
limit: String(limit)
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
__name(getCountryBreakdown, "getCountryBreakdown");
|
|
875
|
+
async function getVideoDetailBreakdowns(videoTitle, timeRange = "30:days") {
|
|
876
|
+
const filter = `video_title:${videoTitle}`;
|
|
877
|
+
const [countries, timeseries] = await Promise.all([
|
|
878
|
+
muxDataFetch("/metrics/views/breakdown", {
|
|
879
|
+
"timeframe[]": timeRange,
|
|
880
|
+
group_by: "country",
|
|
881
|
+
order_by: "views",
|
|
882
|
+
order_direction: "desc",
|
|
883
|
+
limit: "8",
|
|
884
|
+
"filters[]": filter
|
|
885
|
+
}),
|
|
886
|
+
muxDataFetch("/metrics/views/timeseries", {
|
|
887
|
+
"timeframe[]": timeRange,
|
|
888
|
+
group_by: "day",
|
|
889
|
+
"filters[]": filter
|
|
890
|
+
})
|
|
891
|
+
]);
|
|
892
|
+
return {
|
|
893
|
+
countries: countries.data.map((c) => ({
|
|
894
|
+
country: c.field,
|
|
895
|
+
views: c.views,
|
|
896
|
+
watchTimeMs: c.total_watch_time
|
|
897
|
+
})),
|
|
898
|
+
timeseries: timeseries.data.map(([date, value]) => ({
|
|
899
|
+
date,
|
|
900
|
+
views: value ?? 0
|
|
901
|
+
}))
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
__name(getVideoDetailBreakdowns, "getVideoDetailBreakdowns");
|
|
905
|
+
async function getVideoDashboardData(timeRange = "30:days") {
|
|
906
|
+
const [views, experience, comparison, watchTime, videos, countries] = await Promise.all([
|
|
907
|
+
getViewsOverall(timeRange),
|
|
908
|
+
getViewerExperienceScore(timeRange),
|
|
909
|
+
getComparisonTotals(timeRange),
|
|
910
|
+
getWatchTimeTimeseries(timeRange),
|
|
911
|
+
getVideoBreakdown(timeRange, 50),
|
|
912
|
+
getCountryBreakdown(timeRange, 15)
|
|
913
|
+
]);
|
|
914
|
+
return {
|
|
915
|
+
overview: {
|
|
916
|
+
totalViews: views.data.total_views,
|
|
917
|
+
uniqueViewers: comparison.uniqueViewers,
|
|
918
|
+
totalWatchTimeMs: views.data.total_watch_time,
|
|
919
|
+
totalPlayingTimeMs: views.data.total_playing_time,
|
|
920
|
+
viewerExperienceScore: experience.data.value,
|
|
921
|
+
globalExperienceScore: experience.data.global_value
|
|
922
|
+
},
|
|
923
|
+
watchTimeSeries: watchTime.data.map(([date, totalMs]) => ({
|
|
924
|
+
date,
|
|
925
|
+
// Mux playing_time timeseries: value is total playing time in ms
|
|
926
|
+
watchTimeMs: totalMs ?? 0
|
|
927
|
+
})),
|
|
928
|
+
topVideos: videos.data.filter((v) => v.field !== "").map((v) => ({
|
|
929
|
+
title: v.field,
|
|
930
|
+
views: v.views,
|
|
931
|
+
watchTimeMs: v.total_watch_time,
|
|
932
|
+
playingTimeMs: v.total_playing_time
|
|
933
|
+
})),
|
|
934
|
+
countries: countries.data.map((c) => ({
|
|
935
|
+
country: c.field,
|
|
936
|
+
views: c.views,
|
|
937
|
+
watchTimeMs: c.total_watch_time
|
|
938
|
+
}))
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
__name(getVideoDashboardData, "getVideoDashboardData");
|
|
942
|
+
async function getVideoThumbnails(timeRange = "30:days", limit = 10) {
|
|
943
|
+
if (!dbDeps) {
|
|
944
|
+
throw new Error("getVideoThumbnails requires dbDeps (db + contentResource) to be provided to createMuxProvider");
|
|
945
|
+
}
|
|
946
|
+
const { db, contentResource } = dbDeps;
|
|
947
|
+
const [byId, byTitle] = await Promise.all([
|
|
948
|
+
muxDataFetch("/metrics/views/breakdown", {
|
|
949
|
+
"timeframe[]": timeRange,
|
|
950
|
+
group_by: "video_id",
|
|
951
|
+
order_by: "views",
|
|
952
|
+
order_direction: "desc",
|
|
953
|
+
limit: String(limit)
|
|
954
|
+
}),
|
|
955
|
+
muxDataFetch("/metrics/views/breakdown", {
|
|
956
|
+
"timeframe[]": timeRange,
|
|
957
|
+
group_by: "video_title",
|
|
958
|
+
order_by: "views",
|
|
959
|
+
order_direction: "desc",
|
|
960
|
+
limit: String(limit)
|
|
961
|
+
})
|
|
962
|
+
]);
|
|
963
|
+
const videoIds = byId.data.filter((v) => v.field && v.field !== "").map((v) => v.field);
|
|
964
|
+
if (videoIds.length === 0)
|
|
965
|
+
return {};
|
|
966
|
+
const rows = await db.select({
|
|
967
|
+
id: contentResource.id,
|
|
968
|
+
playbackId: sql2`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.muxPlaybackId'))`.as("playbackId")
|
|
969
|
+
}).from(contentResource).where(sql2`${contentResource.id} IN (${sql2.join(videoIds.map((id) => sql2`${id}`), sql2`, `)})`);
|
|
970
|
+
const idToPlayback = /* @__PURE__ */ new Map();
|
|
971
|
+
for (const r of rows) {
|
|
972
|
+
if (r.playbackId && r.playbackId !== "null") {
|
|
973
|
+
idToPlayback.set(r.id, r.playbackId);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const result = {};
|
|
977
|
+
for (let i = 0; i < Math.min(byId.data.length, byTitle.data.length); i++) {
|
|
978
|
+
const videoId = byId.data[i]?.field;
|
|
979
|
+
const title = byTitle.data[i]?.field;
|
|
980
|
+
if (!videoId || !title)
|
|
981
|
+
continue;
|
|
982
|
+
const playbackId = idToPlayback.get(videoId);
|
|
983
|
+
if (playbackId) {
|
|
984
|
+
result[title] = `https://image.mux.com/${playbackId}/thumbnail.jpg?width=240&height=135&fit_mode=smartcrop`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
__name(getVideoThumbnails, "getVideoThumbnails");
|
|
990
|
+
return {
|
|
991
|
+
getComparisonTotals,
|
|
992
|
+
getViewsOverall,
|
|
993
|
+
getViewerExperienceScore,
|
|
994
|
+
getViewsTimeseries,
|
|
995
|
+
getWatchTimeTimeseries,
|
|
996
|
+
getVideoBreakdown,
|
|
997
|
+
getVideoBreakdownForRange,
|
|
998
|
+
getCountryBreakdown,
|
|
999
|
+
getVideoDetailBreakdowns,
|
|
1000
|
+
getVideoDashboardData,
|
|
1001
|
+
getVideoThumbnails
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
__name(createMuxProvider, "createMuxProvider");
|
|
1005
|
+
|
|
1006
|
+
// src/providers/survey.ts
|
|
1007
|
+
import { and as and2, count as count2, eq as eq2, sql as sql3 } from "drizzle-orm";
|
|
1008
|
+
function normalizeRespondentKey(row) {
|
|
1009
|
+
if (row.respondentKey)
|
|
1010
|
+
return row.respondentKey;
|
|
1011
|
+
if (row.userId)
|
|
1012
|
+
return `user:${row.userId}`;
|
|
1013
|
+
if (row.emailListSubscriberId) {
|
|
1014
|
+
return `subscriber:${row.emailListSubscriberId}`;
|
|
1015
|
+
}
|
|
1016
|
+
if (row.surveySessionId)
|
|
1017
|
+
return `session:${row.surveySessionId}`;
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
__name(normalizeRespondentKey, "normalizeRespondentKey");
|
|
1021
|
+
function getRowTimestamp(row) {
|
|
1022
|
+
const date = row.updatedAt ?? row.createdAt;
|
|
1023
|
+
return date instanceof Date ? date.getTime() : 0;
|
|
1024
|
+
}
|
|
1025
|
+
__name(getRowTimestamp, "getRowTimestamp");
|
|
1026
|
+
function sortByNewest(rows) {
|
|
1027
|
+
return [
|
|
1028
|
+
...rows
|
|
1029
|
+
].sort((a, b) => getRowTimestamp(b) - getRowTimestamp(a));
|
|
1030
|
+
}
|
|
1031
|
+
__name(sortByNewest, "sortByNewest");
|
|
1032
|
+
function createSurveyProvider(db, schema) {
|
|
1033
|
+
const { contentResource, contentResourceResource, questionResponse } = schema;
|
|
1034
|
+
function rangeToInterval(range) {
|
|
1035
|
+
switch (range) {
|
|
1036
|
+
case "24h":
|
|
1037
|
+
return "1 DAY";
|
|
1038
|
+
case "7d":
|
|
1039
|
+
return "7 DAY";
|
|
1040
|
+
case "30d":
|
|
1041
|
+
return "30 DAY";
|
|
1042
|
+
case "90d":
|
|
1043
|
+
return "90 DAY";
|
|
1044
|
+
case "all":
|
|
1045
|
+
return "3650 DAY";
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
__name(rangeToInterval, "rangeToInterval");
|
|
1049
|
+
function rangeWhere(range, column) {
|
|
1050
|
+
return sql3`${column} >= DATE_SUB(NOW(), INTERVAL ${sql3.raw(rangeToInterval(range))})`;
|
|
1051
|
+
}
|
|
1052
|
+
__name(rangeWhere, "rangeWhere");
|
|
1053
|
+
async function fetchCanonicalRows(range) {
|
|
1054
|
+
const { users } = schema;
|
|
1055
|
+
const rawRows = users ? await db.select({
|
|
1056
|
+
responseId: questionResponse.id,
|
|
1057
|
+
surveyId: questionResponse.surveyId,
|
|
1058
|
+
surveyTitle: sql3`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,
|
|
1059
|
+
surveySlug: sql3`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,
|
|
1060
|
+
questionId: questionResponse.questionId,
|
|
1061
|
+
questionText: sql3`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,
|
|
1062
|
+
questionType: sql3`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,
|
|
1063
|
+
answer: sql3`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
|
|
1064
|
+
respondentKey: questionResponse.respondentKey,
|
|
1065
|
+
surveySessionId: questionResponse.surveySessionId,
|
|
1066
|
+
userId: questionResponse.userId,
|
|
1067
|
+
userEmail: users.email,
|
|
1068
|
+
emailListSubscriberId: questionResponse.emailListSubscriberId,
|
|
1069
|
+
createdAt: questionResponse.createdAt,
|
|
1070
|
+
updatedAt: questionResponse.updatedAt
|
|
1071
|
+
}).from(questionResponse).leftJoin(sql3`${contentResource} AS survey_cr`, sql3`survey_cr.id = ${questionResponse.surveyId}`).leftJoin(sql3`${contentResource} AS question_cr`, sql3`question_cr.id = ${questionResponse.questionId}`).leftJoin(users, eq2(questionResponse.userId, users.id)).where(rangeWhere(range, questionResponse.createdAt)) : await db.select({
|
|
1072
|
+
responseId: questionResponse.id,
|
|
1073
|
+
surveyId: questionResponse.surveyId,
|
|
1074
|
+
surveyTitle: sql3`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.title'))`,
|
|
1075
|
+
surveySlug: sql3`JSON_UNQUOTE(JSON_EXTRACT(survey_cr.fields, '$.slug'))`,
|
|
1076
|
+
questionId: questionResponse.questionId,
|
|
1077
|
+
questionText: sql3`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.question'))`,
|
|
1078
|
+
questionType: sql3`JSON_UNQUOTE(JSON_EXTRACT(question_cr.fields, '$.type'))`,
|
|
1079
|
+
answer: sql3`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
|
|
1080
|
+
respondentKey: questionResponse.respondentKey,
|
|
1081
|
+
surveySessionId: questionResponse.surveySessionId,
|
|
1082
|
+
userId: questionResponse.userId,
|
|
1083
|
+
userEmail: sql3`NULL`,
|
|
1084
|
+
emailListSubscriberId: questionResponse.emailListSubscriberId,
|
|
1085
|
+
createdAt: questionResponse.createdAt,
|
|
1086
|
+
updatedAt: questionResponse.updatedAt
|
|
1087
|
+
}).from(questionResponse).leftJoin(sql3`${contentResource} AS survey_cr`, sql3`survey_cr.id = ${questionResponse.surveyId}`).leftJoin(sql3`${contentResource} AS question_cr`, sql3`question_cr.id = ${questionResponse.questionId}`).where(rangeWhere(range, questionResponse.createdAt));
|
|
1088
|
+
const latestByAnswer = /* @__PURE__ */ new Map();
|
|
1089
|
+
for (const row of rawRows) {
|
|
1090
|
+
const respondentKey = normalizeRespondentKey(row);
|
|
1091
|
+
if (!respondentKey)
|
|
1092
|
+
continue;
|
|
1093
|
+
const dedupeKey = `${row.surveyId}::${row.questionId}::${respondentKey}`;
|
|
1094
|
+
const current = latestByAnswer.get(dedupeKey);
|
|
1095
|
+
if (!current || getRowTimestamp(row) >= getRowTimestamp(current)) {
|
|
1096
|
+
latestByAnswer.set(dedupeKey, {
|
|
1097
|
+
...row,
|
|
1098
|
+
respondentKey
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return sortByNewest(Array.from(latestByAnswer.values()));
|
|
1103
|
+
}
|
|
1104
|
+
__name(fetchCanonicalRows, "fetchCanonicalRows");
|
|
1105
|
+
async function getSurveySummary(range = "30d") {
|
|
1106
|
+
const [surveyCount] = await db.select({
|
|
1107
|
+
total: count2()
|
|
1108
|
+
}).from(contentResource).where(eq2(contentResource.type, "survey"));
|
|
1109
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
1110
|
+
const respondentKeys = new Set(canonicalRows.map((row) => row.respondentKey));
|
|
1111
|
+
const totalSurveys = surveyCount?.total ?? 0;
|
|
1112
|
+
const totalResponses = canonicalRows.length;
|
|
1113
|
+
return {
|
|
1114
|
+
totalSurveys,
|
|
1115
|
+
totalResponses,
|
|
1116
|
+
uniqueRespondents: respondentKeys.size,
|
|
1117
|
+
avgResponsesPerSurvey: totalSurveys > 0 ? totalResponses / totalSurveys : 0
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
__name(getSurveySummary, "getSurveySummary");
|
|
1121
|
+
async function getSurveyList(range = "30d") {
|
|
1122
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
1123
|
+
const responsesBySurvey = /* @__PURE__ */ new Map();
|
|
1124
|
+
for (const row of canonicalRows) {
|
|
1125
|
+
const current = responsesBySurvey.get(row.surveyId) ?? {
|
|
1126
|
+
responses: 0,
|
|
1127
|
+
respondents: /* @__PURE__ */ new Set()
|
|
1128
|
+
};
|
|
1129
|
+
current.responses += 1;
|
|
1130
|
+
current.respondents.add(row.respondentKey);
|
|
1131
|
+
responsesBySurvey.set(row.surveyId, current);
|
|
1132
|
+
}
|
|
1133
|
+
const surveys = await db.select({
|
|
1134
|
+
surveyId: contentResource.id,
|
|
1135
|
+
surveyTitle: sql3`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.title'))`,
|
|
1136
|
+
surveySlug: sql3`JSON_UNQUOTE(JSON_EXTRACT(${contentResource.fields}, '$.slug'))`
|
|
1137
|
+
}).from(contentResource).where(eq2(contentResource.type, "survey"));
|
|
1138
|
+
const questionCounts = await db.select({
|
|
1139
|
+
surveyId: contentResourceResource.resourceOfId,
|
|
1140
|
+
questionCount: count2()
|
|
1141
|
+
}).from(contentResourceResource).innerJoin(contentResource, and2(eq2(contentResourceResource.resourceId, contentResource.id), eq2(contentResource.type, "question"))).groupBy(contentResourceResource.resourceOfId);
|
|
1142
|
+
const questionCountMap = new Map(questionCounts.map((qc) => [
|
|
1143
|
+
qc.surveyId,
|
|
1144
|
+
qc.questionCount
|
|
1145
|
+
]));
|
|
1146
|
+
return surveys.map((s) => {
|
|
1147
|
+
const counts = responsesBySurvey.get(s.surveyId);
|
|
1148
|
+
return {
|
|
1149
|
+
surveyId: s.surveyId,
|
|
1150
|
+
surveyTitle: s.surveyTitle ?? "",
|
|
1151
|
+
surveySlug: s.surveySlug ?? "",
|
|
1152
|
+
responses: counts?.responses ?? 0,
|
|
1153
|
+
uniqueRespondents: counts?.respondents.size ?? 0,
|
|
1154
|
+
questionCount: questionCountMap.get(s.surveyId) ?? 0
|
|
1155
|
+
};
|
|
1156
|
+
}).sort((a, b) => b.responses - a.responses);
|
|
1157
|
+
}
|
|
1158
|
+
__name(getSurveyList, "getSurveyList");
|
|
1159
|
+
async function getSurveyResponsesByDay(range = "30d") {
|
|
1160
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
1161
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const row of canonicalRows) {
|
|
1163
|
+
if (!(row.createdAt instanceof Date))
|
|
1164
|
+
continue;
|
|
1165
|
+
const date = row.createdAt.toISOString().slice(0, 10);
|
|
1166
|
+
grouped.set(date, (grouped.get(date) ?? 0) + 1);
|
|
1167
|
+
}
|
|
1168
|
+
return Array.from(grouped.entries()).sort((entryA, entryB) => entryA[0].localeCompare(entryB[0])).map(([date, responses]) => ({
|
|
1169
|
+
date,
|
|
1170
|
+
responses
|
|
1171
|
+
}));
|
|
1172
|
+
}
|
|
1173
|
+
__name(getSurveyResponsesByDay, "getSurveyResponsesByDay");
|
|
1174
|
+
async function getSurveyQuestionBreakdown(range = "30d", limit = 20) {
|
|
1175
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
1176
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1177
|
+
for (const row of canonicalRows) {
|
|
1178
|
+
const current = grouped.get(row.questionId) ?? {
|
|
1179
|
+
questionId: row.questionId,
|
|
1180
|
+
question: row.questionText ?? "",
|
|
1181
|
+
type: row.questionType ?? null,
|
|
1182
|
+
responses: 0,
|
|
1183
|
+
respondents: /* @__PURE__ */ new Set(),
|
|
1184
|
+
answers: /* @__PURE__ */ new Map()
|
|
1185
|
+
};
|
|
1186
|
+
current.responses += 1;
|
|
1187
|
+
current.respondents.add(row.respondentKey);
|
|
1188
|
+
const answer = row.answer ?? "(no answer)";
|
|
1189
|
+
current.answers.set(answer, (current.answers.get(answer) ?? 0) + 1);
|
|
1190
|
+
grouped.set(row.questionId, current);
|
|
1191
|
+
}
|
|
1192
|
+
return Array.from(grouped.values()).sort((a, b) => b.responses - a.responses).slice(0, limit).map((entry) => ({
|
|
1193
|
+
questionId: entry.questionId,
|
|
1194
|
+
question: entry.question,
|
|
1195
|
+
type: entry.type,
|
|
1196
|
+
responses: entry.responses,
|
|
1197
|
+
uniqueRespondents: entry.respondents.size,
|
|
1198
|
+
answerDistribution: Array.from(entry.answers.entries()).sort((a, b) => b[1] - a[1]).map(([answer, count3]) => ({
|
|
1199
|
+
answer,
|
|
1200
|
+
count: count3
|
|
1201
|
+
}))
|
|
1202
|
+
}));
|
|
1203
|
+
}
|
|
1204
|
+
__name(getSurveyQuestionBreakdown, "getSurveyQuestionBreakdown");
|
|
1205
|
+
async function getSurveyResponses(range = "30d", limit = 100) {
|
|
1206
|
+
const canonicalRows = await fetchCanonicalRows(range);
|
|
1207
|
+
return canonicalRows.slice(0, limit).map((row) => ({
|
|
1208
|
+
responseId: row.responseId,
|
|
1209
|
+
surveyId: row.surveyId,
|
|
1210
|
+
surveyTitle: row.surveyTitle ?? "",
|
|
1211
|
+
surveySlug: row.surveySlug ?? "",
|
|
1212
|
+
questionId: row.questionId,
|
|
1213
|
+
questionText: row.questionText ?? "",
|
|
1214
|
+
questionType: row.questionType ?? null,
|
|
1215
|
+
answer: row.answer ?? "",
|
|
1216
|
+
userId: row.userId ?? null,
|
|
1217
|
+
userEmail: row.userEmail ?? null,
|
|
1218
|
+
emailListSubscriberId: row.emailListSubscriberId ?? null,
|
|
1219
|
+
createdAt: row.createdAt ? String(row.createdAt) : ""
|
|
1220
|
+
}));
|
|
1221
|
+
}
|
|
1222
|
+
__name(getSurveyResponses, "getSurveyResponses");
|
|
1223
|
+
return {
|
|
1224
|
+
getSurveySummary,
|
|
1225
|
+
getSurveyList,
|
|
1226
|
+
getSurveyResponsesByDay,
|
|
1227
|
+
getSurveyQuestionBreakdown,
|
|
1228
|
+
getSurveyResponses
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
__name(createSurveyProvider, "createSurveyProvider");
|
|
1232
|
+
export {
|
|
1233
|
+
createDatabaseProvider,
|
|
1234
|
+
createDerivedProvider,
|
|
1235
|
+
createGA4Provider,
|
|
1236
|
+
createMuxProvider,
|
|
1237
|
+
createSurveyProvider
|
|
1238
|
+
};
|
|
1239
|
+
//# sourceMappingURL=index.js.map
|