@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,533 @@
|
|
|
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
|
+
export {
|
|
531
|
+
createDatabaseProvider
|
|
532
|
+
};
|
|
533
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/database.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"],"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"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GA4TimeRange } from './ga4.js';
|
|
2
|
+
|
|
3
|
+
type AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all';
|
|
4
|
+
interface TrafficRevenueCorrelation {
|
|
5
|
+
traffic: {
|
|
6
|
+
date: string;
|
|
7
|
+
sessions: number;
|
|
8
|
+
users: number;
|
|
9
|
+
pageviews: number;
|
|
10
|
+
}[];
|
|
11
|
+
revenue: {
|
|
12
|
+
date: string;
|
|
13
|
+
revenue: number;
|
|
14
|
+
count: number;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
interface DerivedProviderDeps {
|
|
18
|
+
database: {
|
|
19
|
+
getRevenueByDay: (range: AnalyticsRange) => Promise<{
|
|
20
|
+
date: string;
|
|
21
|
+
revenue: number;
|
|
22
|
+
count: number;
|
|
23
|
+
}[]>;
|
|
24
|
+
};
|
|
25
|
+
ga4: {
|
|
26
|
+
getSessionsByDay: (range: GA4TimeRange) => Promise<{
|
|
27
|
+
date: string;
|
|
28
|
+
sessions: number;
|
|
29
|
+
users: number;
|
|
30
|
+
pageviews: number;
|
|
31
|
+
}[]>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a derived analytics provider that combines data from multiple
|
|
36
|
+
* sources (database + GA4) to compute correlation metrics.
|
|
37
|
+
*
|
|
38
|
+
* @param deps - Provider dependencies (database and ga4 providers)
|
|
39
|
+
*/
|
|
40
|
+
declare function createDerivedProvider(deps: DerivedProviderDeps): {
|
|
41
|
+
getTrafficRevenueCorrelation: (range: AnalyticsRange) => Promise<TrafficRevenueCorrelation>;
|
|
42
|
+
};
|
|
43
|
+
type DerivedAnalyticsProvider = ReturnType<typeof createDerivedProvider>;
|
|
44
|
+
|
|
45
|
+
export { type AnalyticsRange, type DerivedAnalyticsProvider, type DerivedProviderDeps, type TrafficRevenueCorrelation, createDerivedProvider };
|