@coursebuilder/analytics 1.1.0 → 1.1.2-canary.0.0c9beb2ae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +22 -2
- package/dist/api/index.js +40 -5
- package/dist/api/index.js.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +43 -1
- package/dist/catalog.js.map +1 -1
- package/dist/components/index.d.ts +46 -1
- package/dist/components/index.js +200 -8
- package/dist/components/index.js.map +1 -1
- package/dist/engine.js +94 -6
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +94 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/database.d.ts +174 -2
- package/dist/providers/database.js +711 -20
- package/dist/providers/database.js.map +1 -1
- package/dist/providers/index.js +713 -22
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/survey.d.ts +1 -1
- package/dist/providers/survey.js +2 -2
- package/dist/providers/survey.js.map +1 -1
- package/dist/types.d.ts +189 -3
- package/package.json +7 -5
- package/src/api/catalog-handler.ts +44 -2
- package/src/api/token-handler.ts +3 -2
- package/src/catalog.ts +49 -1
- package/src/components/omnibus-dashboard.tsx +340 -6
- package/src/engine.ts +69 -8
- package/src/providers/attribution-recovery.test.ts +63 -0
- package/src/providers/attribution-recovery.ts +97 -0
- package/src/providers/database.ts +880 -42
- package/src/providers/survey.ts +3 -1
- package/src/types.ts +215 -2
package/dist/providers/index.js
CHANGED
|
@@ -3,18 +3,89 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/providers/database.ts
|
|
5
5
|
import { and, count, desc, eq, gt, gte, inArray, lte, sql, sum } from "drizzle-orm";
|
|
6
|
+
|
|
7
|
+
// src/providers/attribution-recovery.ts
|
|
8
|
+
var SHORTLINK_RECOVERY_LANE = "recovered_from_shortlink_attribution_table";
|
|
9
|
+
|
|
10
|
+
// src/providers/database.ts
|
|
6
11
|
function createDatabaseProvider(db, schema) {
|
|
7
|
-
const { purchases, products, users, coupon, resourceProgress, shortlink, shortlinkAttribution, shortlinkClick } = schema;
|
|
12
|
+
const { purchases, products, users, coupon, resourceProgress, shortlink, shortlinkAttribution, shortlinkClick, questionResponse, contactEvent, sideEffectIntent } = schema;
|
|
8
13
|
const PAID_STATUSES = [
|
|
9
14
|
"Valid",
|
|
10
15
|
"Restricted"
|
|
11
16
|
];
|
|
12
|
-
function
|
|
17
|
+
function commerceRecord() {
|
|
13
18
|
return inArray(purchases.status, [
|
|
14
19
|
...PAID_STATUSES
|
|
15
20
|
]);
|
|
16
21
|
}
|
|
22
|
+
__name(commerceRecord, "commerceRecord");
|
|
23
|
+
function paidPurchase() {
|
|
24
|
+
return and(commerceRecord(), gt(purchases.totalAmount, 0));
|
|
25
|
+
}
|
|
17
26
|
__name(paidPurchase, "paidPurchase");
|
|
27
|
+
function accessGrant() {
|
|
28
|
+
return and(commerceRecord(), sql`${purchases.totalAmount} = 0`);
|
|
29
|
+
}
|
|
30
|
+
__name(accessGrant, "accessGrant");
|
|
31
|
+
function backfillFreeUpgrade() {
|
|
32
|
+
return and(accessGrant(), sql`JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'`);
|
|
33
|
+
}
|
|
34
|
+
__name(backfillFreeUpgrade, "backfillFreeUpgrade");
|
|
35
|
+
const syntheticSignalSql = sql`(
|
|
36
|
+
JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.synthetic')) = 'true'
|
|
37
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.synthetic')) = 'true'
|
|
38
|
+
OR JSON_SEARCH(JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds'), 'one', 'TEST_%') IS NOT NULL
|
|
39
|
+
)`;
|
|
40
|
+
function syntheticPurchase() {
|
|
41
|
+
return and(commerceRecord(), syntheticSignalSql);
|
|
42
|
+
}
|
|
43
|
+
__name(syntheticPurchase, "syntheticPurchase");
|
|
44
|
+
const purchaseFieldAttributionSignalSql = sql`(
|
|
45
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution') IS NOT NULL
|
|
46
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
47
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
48
|
+
)`;
|
|
49
|
+
const exactShortlinkPurchaseAttributionSignalSql = sql`EXISTS (
|
|
50
|
+
SELECT 1
|
|
51
|
+
FROM ${shortlinkAttribution}
|
|
52
|
+
WHERE ${shortlinkAttribution.type} = 'purchase'
|
|
53
|
+
AND JSON_VALID(${shortlinkAttribution.metadata})
|
|
54
|
+
AND JSON_UNQUOTE(JSON_EXTRACT(${shortlinkAttribution.metadata}, '$.purchaseId')) = ${purchases.id}
|
|
55
|
+
)`;
|
|
56
|
+
const shortlinkRecoveredAttributionSignalSql = sql`(
|
|
57
|
+
NOT ${purchaseFieldAttributionSignalSql}
|
|
58
|
+
AND ${exactShortlinkPurchaseAttributionSignalSql}
|
|
59
|
+
)`;
|
|
60
|
+
const attributionSignalSql = sql`(
|
|
61
|
+
${purchaseFieldAttributionSignalSql}
|
|
62
|
+
OR ${exactShortlinkPurchaseAttributionSignalSql}
|
|
63
|
+
)`;
|
|
64
|
+
const strongAttributionQualitySql = sql`(
|
|
65
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL
|
|
66
|
+
OR JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
|
|
67
|
+
OR JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink') IS NOT NULL
|
|
68
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
69
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
|
|
70
|
+
)`;
|
|
71
|
+
const mediumAttributionQualitySql = sql`(
|
|
72
|
+
NOT ${strongAttributionQualitySql}
|
|
73
|
+
AND ${exactShortlinkPurchaseAttributionSignalSql}
|
|
74
|
+
)`;
|
|
75
|
+
const weakAttributionQualitySql = sql`(
|
|
76
|
+
NOT ${strongAttributionQualitySql}
|
|
77
|
+
AND NOT ${exactShortlinkPurchaseAttributionSignalSql}
|
|
78
|
+
AND (
|
|
79
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
|
|
80
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
81
|
+
OR JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL
|
|
82
|
+
)
|
|
83
|
+
)`;
|
|
84
|
+
const unknownAttributionQualitySql = sql`(
|
|
85
|
+
NOT ${strongAttributionQualitySql}
|
|
86
|
+
AND NOT ${mediumAttributionQualitySql}
|
|
87
|
+
AND NOT ${weakAttributionQualitySql}
|
|
88
|
+
)`;
|
|
18
89
|
function rangeToDate(range) {
|
|
19
90
|
if (range === "all")
|
|
20
91
|
return null;
|
|
@@ -255,21 +326,80 @@ function createDatabaseProvider(db, schema) {
|
|
|
255
326
|
});
|
|
256
327
|
}
|
|
257
328
|
__name(getShortlinkPerformance, "getShortlinkPerformance");
|
|
258
|
-
async function getRevenueBySource(range = "30d") {
|
|
329
|
+
async function getRevenueBySource(range = "30d", filters = {}) {
|
|
259
330
|
const since = rangeToDate(range);
|
|
260
331
|
const conditions = [
|
|
261
|
-
|
|
332
|
+
commerceRecord()
|
|
262
333
|
];
|
|
263
334
|
if (since)
|
|
264
335
|
conditions.push(gte(purchases.createdAt, since));
|
|
336
|
+
if (filters.productId)
|
|
337
|
+
conditions.push(eq(purchases.productId, filters.productId));
|
|
338
|
+
const kindExpr = sql`CASE
|
|
339
|
+
WHEN ${syntheticSignalSql} THEN 'synthetic'
|
|
340
|
+
WHEN ${purchases.totalAmount} = 0 THEN 'access_grant'
|
|
341
|
+
WHEN ${purchases.totalAmount} > 0 THEN 'paid_conversion'
|
|
342
|
+
ELSE 'unknown'
|
|
343
|
+
END`;
|
|
344
|
+
const sourceExpr = sql`CASE
|
|
345
|
+
WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
|
|
346
|
+
THEN 'internal'
|
|
347
|
+
WHEN ${shortlinkRecoveredAttributionSignalSql} THEN ${SHORTLINK_RECOVERY_LANE}
|
|
348
|
+
ELSE COALESCE(
|
|
349
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.source')), 'null'),
|
|
350
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.source')), 'null'),
|
|
351
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')), 'null'),
|
|
352
|
+
CASE
|
|
353
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
|
|
354
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'self_reported'
|
|
355
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'ga4'
|
|
356
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'ga4'
|
|
357
|
+
ELSE NULL
|
|
358
|
+
END
|
|
359
|
+
)
|
|
360
|
+
END`;
|
|
361
|
+
const mediumExpr = sql`CASE
|
|
362
|
+
WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
|
|
363
|
+
THEN 'free_upgrade'
|
|
364
|
+
WHEN ${shortlinkRecoveredAttributionSignalSql} THEN 'shortlink'
|
|
365
|
+
ELSE COALESCE(
|
|
366
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.medium')), 'null'),
|
|
367
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.medium')), 'null'),
|
|
368
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium')), 'null'),
|
|
369
|
+
CASE
|
|
370
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
|
|
371
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'checkout_survey'
|
|
372
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'client_id'
|
|
373
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'client_id'
|
|
374
|
+
ELSE NULL
|
|
375
|
+
END
|
|
376
|
+
)
|
|
377
|
+
END`;
|
|
378
|
+
const campaignExpr = sql`CASE
|
|
379
|
+
WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
|
|
380
|
+
THEN COALESCE(
|
|
381
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.migrationBatchId')), 'null'),
|
|
382
|
+
'free_upgrade'
|
|
383
|
+
)
|
|
384
|
+
WHEN ${shortlinkRecoveredAttributionSignalSql} THEN NULL
|
|
385
|
+
ELSE COALESCE(
|
|
386
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.campaign')), 'null'),
|
|
387
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.campaign')), 'null'),
|
|
388
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign')), 'null'),
|
|
389
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug')), 'null'),
|
|
390
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource')), 'null')
|
|
391
|
+
)
|
|
392
|
+
END`;
|
|
265
393
|
const rows = await db.select({
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
394
|
+
kind: kindExpr.as("kind"),
|
|
395
|
+
source: sourceExpr.as("source"),
|
|
396
|
+
medium: mediumExpr.as("medium"),
|
|
397
|
+
campaign: campaignExpr.as("campaign"),
|
|
269
398
|
revenue: sum(purchases.totalAmount),
|
|
270
399
|
count: count()
|
|
271
|
-
}).from(purchases).where(and(...conditions)).groupBy(sql`
|
|
400
|
+
}).from(purchases).where(and(...conditions)).groupBy(sql`1`, sql`2`, sql`3`, sql`4`).orderBy(desc(sum(purchases.totalAmount)), desc(count()));
|
|
272
401
|
return rows.map((r) => ({
|
|
402
|
+
kind: r.kind ?? "unknown",
|
|
273
403
|
source: r.source ?? null,
|
|
274
404
|
medium: r.medium ?? null,
|
|
275
405
|
campaign: r.campaign ?? null,
|
|
@@ -296,10 +426,7 @@ function createDatabaseProvider(db, schema) {
|
|
|
296
426
|
}).from(purchases).where(and(...purchaseConditions));
|
|
297
427
|
const [attributedCount] = await db.select({
|
|
298
428
|
total: count()
|
|
299
|
-
}).from(purchases).where(and(...purchaseConditions,
|
|
300
|
-
JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
301
|
-
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
302
|
-
)`));
|
|
429
|
+
}).from(purchases).where(and(...purchaseConditions, attributionSignalSql));
|
|
303
430
|
const totalSignups = userCount?.total ?? 0;
|
|
304
431
|
const totalPurchases = purchaseCount?.total ?? 0;
|
|
305
432
|
const attributedPurchases = attributedCount?.total ?? 0;
|
|
@@ -312,33 +439,180 @@ function createDatabaseProvider(db, schema) {
|
|
|
312
439
|
};
|
|
313
440
|
}
|
|
314
441
|
__name(getConversionFunnel, "getConversionFunnel");
|
|
315
|
-
async function
|
|
442
|
+
async function getCommerceLaneSummary(range = "30d", filters = {}) {
|
|
443
|
+
const since = rangeToDate(range);
|
|
444
|
+
const conditions = [
|
|
445
|
+
commerceRecord()
|
|
446
|
+
];
|
|
447
|
+
if (since)
|
|
448
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
449
|
+
if (filters.productId)
|
|
450
|
+
conditions.push(eq(purchases.productId, filters.productId));
|
|
451
|
+
const [commerce] = await db.select({
|
|
452
|
+
count: count()
|
|
453
|
+
}).from(purchases).where(and(...conditions));
|
|
454
|
+
const [paid] = await db.select({
|
|
455
|
+
count: count(),
|
|
456
|
+
revenue: sum(purchases.totalAmount)
|
|
457
|
+
}).from(purchases).where(and(...conditions, paidPurchase()));
|
|
458
|
+
const [grants] = await db.select({
|
|
459
|
+
count: count(),
|
|
460
|
+
revenue: sum(purchases.totalAmount)
|
|
461
|
+
}).from(purchases).where(and(...conditions, accessGrant()));
|
|
462
|
+
const [freeUpgrades] = await db.select({
|
|
463
|
+
count: count()
|
|
464
|
+
}).from(purchases).where(and(...conditions, backfillFreeUpgrade()));
|
|
465
|
+
const [synthetic] = await db.select({
|
|
466
|
+
count: count()
|
|
467
|
+
}).from(purchases).where(and(...conditions, syntheticPurchase()));
|
|
468
|
+
const reasonRows = await db.select({
|
|
469
|
+
reason: sql`COALESCE(
|
|
470
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
|
|
471
|
+
'access_grant'
|
|
472
|
+
)`.as("reason"),
|
|
473
|
+
count: count()
|
|
474
|
+
}).from(purchases).where(and(...conditions, accessGrant())).groupBy(sql`COALESCE(
|
|
475
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
|
|
476
|
+
'access_grant'
|
|
477
|
+
)`);
|
|
478
|
+
return {
|
|
479
|
+
commerceRecords: commerce?.count ?? 0,
|
|
480
|
+
paidPurchases: paid?.count ?? 0,
|
|
481
|
+
paidRevenue: Number(paid?.revenue ?? 0),
|
|
482
|
+
accessGrants: grants?.count ?? 0,
|
|
483
|
+
accessGrantRevenue: Number(grants?.revenue ?? 0),
|
|
484
|
+
freeUpgrades: freeUpgrades?.count ?? 0,
|
|
485
|
+
syntheticPurchases: synthetic?.count ?? 0,
|
|
486
|
+
byAccessGrantReason: Object.fromEntries(reasonRows.map((row) => [
|
|
487
|
+
row.reason,
|
|
488
|
+
row.count
|
|
489
|
+
]))
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
__name(getCommerceLaneSummary, "getCommerceLaneSummary");
|
|
493
|
+
async function getAttributedRevenueSummary(range = "30d", filters = {}) {
|
|
316
494
|
const since = rangeToDate(range);
|
|
317
495
|
const conditions = [
|
|
318
496
|
paidPurchase()
|
|
319
497
|
];
|
|
320
498
|
if (since)
|
|
321
499
|
conditions.push(gte(purchases.createdAt, since));
|
|
500
|
+
if (filters.productId)
|
|
501
|
+
conditions.push(eq(purchases.productId, filters.productId));
|
|
322
502
|
const [totals] = await db.select({
|
|
323
503
|
total: sum(purchases.totalAmount),
|
|
324
504
|
count: count()
|
|
325
505
|
}).from(purchases).where(and(...conditions));
|
|
326
506
|
const [attributed] = await db.select({
|
|
327
|
-
total: sum(purchases.totalAmount)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
507
|
+
total: sum(purchases.totalAmount),
|
|
508
|
+
count: count()
|
|
509
|
+
}).from(purchases).where(and(...conditions, attributionSignalSql));
|
|
510
|
+
const [purchaseFieldAttributed] = await db.select({
|
|
511
|
+
total: sum(purchases.totalAmount),
|
|
512
|
+
count: count()
|
|
513
|
+
}).from(purchases).where(and(...conditions, purchaseFieldAttributionSignalSql));
|
|
514
|
+
const [recoveredFromShortlinkAttributionTable] = await db.select({
|
|
515
|
+
total: sum(purchases.totalAmount),
|
|
516
|
+
count: count()
|
|
517
|
+
}).from(purchases).where(and(...conditions, shortlinkRecoveredAttributionSignalSql));
|
|
518
|
+
const signalConditions = since ? [
|
|
519
|
+
commerceRecord(),
|
|
520
|
+
gte(purchases.createdAt, since)
|
|
521
|
+
] : [
|
|
522
|
+
commerceRecord()
|
|
523
|
+
];
|
|
524
|
+
if (filters.productId) {
|
|
525
|
+
signalConditions.push(eq(purchases.productId, filters.productId));
|
|
526
|
+
}
|
|
527
|
+
const signalCount = /* @__PURE__ */ __name(async (condition) => {
|
|
528
|
+
const [row] = await db.select({
|
|
529
|
+
count: count()
|
|
530
|
+
}).from(purchases).where(and(...signalConditions, condition));
|
|
531
|
+
return row?.count ?? 0;
|
|
532
|
+
}, "signalCount");
|
|
533
|
+
const qualityLane = /* @__PURE__ */ __name(async (lane, condition, notes) => {
|
|
534
|
+
const [row] = await db.select({
|
|
535
|
+
total: sum(purchases.totalAmount),
|
|
536
|
+
count: count()
|
|
537
|
+
}).from(purchases).where(and(...conditions, condition));
|
|
538
|
+
const revenue = Number(row?.total ?? 0);
|
|
539
|
+
return {
|
|
540
|
+
lane,
|
|
541
|
+
purchases: row?.count ?? 0,
|
|
542
|
+
revenue,
|
|
543
|
+
revenuePercent: totalRevenue > 0 ? revenue / totalRevenue : 0,
|
|
544
|
+
notes
|
|
545
|
+
};
|
|
546
|
+
}, "qualityLane");
|
|
547
|
+
const [shortlinkCount, utmCount, clickIdCount, gaClientIdCount, selfReportedSourceCount, recoveredFromShortlinkAttributionTableCount] = await Promise.all([
|
|
548
|
+
signalCount(sql`(
|
|
549
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL
|
|
550
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
|
|
551
|
+
)`),
|
|
552
|
+
signalCount(sql`(
|
|
553
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
|
|
554
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
555
|
+
)`),
|
|
556
|
+
signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL`),
|
|
557
|
+
signalCount(sql`(
|
|
558
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
|
|
559
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
560
|
+
)`),
|
|
561
|
+
signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL`),
|
|
562
|
+
signalCount(shortlinkRecoveredAttributionSignalSql)
|
|
563
|
+
]);
|
|
564
|
+
const commerce = await getCommerceLaneSummary(range, filters);
|
|
332
565
|
const totalRevenue = Number(totals?.total ?? 0);
|
|
333
566
|
const attributedRevenue = Number(attributed?.total ?? 0);
|
|
567
|
+
const purchaseFieldAttributedRevenue = Number(purchaseFieldAttributed?.total ?? 0);
|
|
568
|
+
const recoveredFromShortlinkAttributionTableRevenue = Number(recoveredFromShortlinkAttributionTable?.total ?? 0);
|
|
334
569
|
const unattributedRevenue = totalRevenue - attributedRevenue;
|
|
335
|
-
const
|
|
570
|
+
const paidPurchases = totals?.count ?? 0;
|
|
571
|
+
const [strongQuality, mediumQuality, weakQuality, unknownQuality] = await Promise.all([
|
|
572
|
+
qualityLane("strong", strongAttributionQualitySql, [
|
|
573
|
+
"Purchase-field UTM, shortlink, or paid click evidence."
|
|
574
|
+
]),
|
|
575
|
+
qualityLane("medium", mediumAttributionQualitySql, [
|
|
576
|
+
"Recovered from exact shortlink attribution table purchase match."
|
|
577
|
+
]),
|
|
578
|
+
qualityLane("weak", weakAttributionQualitySql, [
|
|
579
|
+
"GA client ID or self-reported source without stronger campaign evidence."
|
|
580
|
+
]),
|
|
581
|
+
qualityLane("unknown", unknownAttributionQualitySql, [
|
|
582
|
+
"No usable attribution evidence for paid revenue."
|
|
583
|
+
])
|
|
584
|
+
]);
|
|
336
585
|
return {
|
|
337
586
|
totalRevenue,
|
|
338
587
|
attributedRevenue,
|
|
339
588
|
unattributedRevenue,
|
|
340
589
|
attributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,
|
|
341
|
-
totalPurchases
|
|
590
|
+
totalPurchases: commerce.commerceRecords,
|
|
591
|
+
paidPurchases,
|
|
592
|
+
purchaseFieldAttributedPurchases: purchaseFieldAttributed?.count ?? 0,
|
|
593
|
+
purchaseFieldAttributedRevenue,
|
|
594
|
+
recoveredFromShortlinkAttributionTablePurchases: recoveredFromShortlinkAttributionTable?.count ?? 0,
|
|
595
|
+
recoveredFromShortlinkAttributionTableRevenue,
|
|
596
|
+
commerceRecords: commerce.commerceRecords,
|
|
597
|
+
accessGrants: commerce.accessGrants,
|
|
598
|
+
freeUpgrades: commerce.freeUpgrades,
|
|
599
|
+
syntheticPurchases: commerce.syntheticPurchases,
|
|
600
|
+
commerce,
|
|
601
|
+
signals: {
|
|
602
|
+
shortlink: shortlinkCount,
|
|
603
|
+
utm: utmCount,
|
|
604
|
+
paidClickId: clickIdCount,
|
|
605
|
+
gaClientId: gaClientIdCount,
|
|
606
|
+
selfReportedSource: selfReportedSourceCount,
|
|
607
|
+
recoveredFromShortlinkAttributionTable: recoveredFromShortlinkAttributionTableCount,
|
|
608
|
+
internalFreeUpgrade: commerce.freeUpgrades
|
|
609
|
+
},
|
|
610
|
+
quality: {
|
|
611
|
+
strong: strongQuality,
|
|
612
|
+
medium: mediumQuality,
|
|
613
|
+
weak: weakQuality,
|
|
614
|
+
unknown: unknownQuality
|
|
615
|
+
}
|
|
342
616
|
};
|
|
343
617
|
}
|
|
344
618
|
__name(getAttributedRevenueSummary, "getAttributedRevenueSummary");
|
|
@@ -365,6 +639,419 @@ function createDatabaseProvider(db, schema) {
|
|
|
365
639
|
}));
|
|
366
640
|
}
|
|
367
641
|
__name(getContentPurchaseCorrelation, "getContentPurchaseCorrelation");
|
|
642
|
+
function parseJsonRecord(value) {
|
|
643
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
644
|
+
}
|
|
645
|
+
__name(parseJsonRecord, "parseJsonRecord");
|
|
646
|
+
function hasRecordValue(value) {
|
|
647
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.length > 0);
|
|
648
|
+
}
|
|
649
|
+
__name(hasRecordValue, "hasRecordValue");
|
|
650
|
+
function hasSyntheticClickId(value) {
|
|
651
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.startsWith("TEST_"));
|
|
652
|
+
}
|
|
653
|
+
__name(hasSyntheticClickId, "hasSyntheticClickId");
|
|
654
|
+
async function getCheckoutAttributionReceipt(opts) {
|
|
655
|
+
if (!opts.purchaseId) {
|
|
656
|
+
return {
|
|
657
|
+
purchase: null,
|
|
658
|
+
checks: {
|
|
659
|
+
purchaseFound: false,
|
|
660
|
+
attributionSnapshot: false,
|
|
661
|
+
utm: false,
|
|
662
|
+
actualUtm: false,
|
|
663
|
+
clickId: false,
|
|
664
|
+
synthetic: false,
|
|
665
|
+
shortlink: false,
|
|
666
|
+
selfReportedSource: false,
|
|
667
|
+
gaClientId: false
|
|
668
|
+
},
|
|
669
|
+
attribution: null,
|
|
670
|
+
legacy: {},
|
|
671
|
+
fieldKeys: []
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const [row] = await db.select({
|
|
675
|
+
id: purchases.id,
|
|
676
|
+
createdAt: purchases.createdAt,
|
|
677
|
+
productId: purchases.productId,
|
|
678
|
+
productName: products.name,
|
|
679
|
+
totalAmount: purchases.totalAmount,
|
|
680
|
+
status: purchases.status,
|
|
681
|
+
country: purchases.country,
|
|
682
|
+
fields: purchases.fields
|
|
683
|
+
}).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(eq(purchases.id, opts.purchaseId)).limit(1);
|
|
684
|
+
if (!row) {
|
|
685
|
+
return {
|
|
686
|
+
purchase: null,
|
|
687
|
+
checks: {
|
|
688
|
+
purchaseFound: false,
|
|
689
|
+
attributionSnapshot: false,
|
|
690
|
+
utm: false,
|
|
691
|
+
actualUtm: false,
|
|
692
|
+
clickId: false,
|
|
693
|
+
synthetic: false,
|
|
694
|
+
shortlink: false,
|
|
695
|
+
selfReportedSource: false,
|
|
696
|
+
gaClientId: false
|
|
697
|
+
},
|
|
698
|
+
attribution: null,
|
|
699
|
+
legacy: {},
|
|
700
|
+
fieldKeys: []
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const fields = parseJsonRecord(row.fields);
|
|
704
|
+
const attribution = parseJsonRecord(fields.attribution);
|
|
705
|
+
const utm = parseJsonRecord(attribution.utm);
|
|
706
|
+
const clickIds = parseJsonRecord(attribution.clickIds);
|
|
707
|
+
const shortlinkSnapshot = parseJsonRecord(attribution.shortlink);
|
|
708
|
+
const ga = parseJsonRecord(attribution.ga);
|
|
709
|
+
const legacy = Object.fromEntries([
|
|
710
|
+
"utmSource",
|
|
711
|
+
"utmMedium",
|
|
712
|
+
"utmCampaign",
|
|
713
|
+
"gaClientId",
|
|
714
|
+
"shortlinkRef",
|
|
715
|
+
"landingPath",
|
|
716
|
+
"referrer",
|
|
717
|
+
"ltUtmSource",
|
|
718
|
+
"ltUtmMedium",
|
|
719
|
+
"ltUtmCampaign"
|
|
720
|
+
].map((key) => [
|
|
721
|
+
key,
|
|
722
|
+
fields[key]
|
|
723
|
+
]).filter(([, value]) => value !== void 0 && value !== null));
|
|
724
|
+
const hasActualUtm = Boolean(utm.source || utm.medium || utm.campaign || fields.utmSource);
|
|
725
|
+
const synthetic = Boolean(attribution.synthetic || fields.synthetic || hasSyntheticClickId(clickIds));
|
|
726
|
+
return {
|
|
727
|
+
purchase: {
|
|
728
|
+
id: row.id,
|
|
729
|
+
createdAt: row.createdAt,
|
|
730
|
+
productId: row.productId,
|
|
731
|
+
productName: row.productName ?? null,
|
|
732
|
+
totalAmount: Number(row.totalAmount ?? 0),
|
|
733
|
+
status: row.status,
|
|
734
|
+
country: row.country
|
|
735
|
+
},
|
|
736
|
+
checks: {
|
|
737
|
+
purchaseFound: true,
|
|
738
|
+
attributionSnapshot: Object.keys(attribution).length > 0,
|
|
739
|
+
utm: hasActualUtm,
|
|
740
|
+
actualUtm: hasActualUtm,
|
|
741
|
+
clickId: hasRecordValue(clickIds),
|
|
742
|
+
synthetic,
|
|
743
|
+
shortlink: Boolean(shortlinkSnapshot.slug || fields.shortlinkRef),
|
|
744
|
+
selfReportedSource: Boolean(attribution.selfReportedSource),
|
|
745
|
+
gaClientId: Boolean(ga.clientId || fields.gaClientId)
|
|
746
|
+
},
|
|
747
|
+
attribution: Object.keys(attribution).length > 0 ? attribution : null,
|
|
748
|
+
legacy,
|
|
749
|
+
fieldKeys: Object.keys(fields).sort()
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
__name(getCheckoutAttributionReceipt, "getCheckoutAttributionReceipt");
|
|
753
|
+
async function getCheckoutSurveyFallbackReport(range = "30d", limit = 20, filters = {}) {
|
|
754
|
+
const label = "recovered_from_checkout_survey_response";
|
|
755
|
+
if (!questionResponse) {
|
|
756
|
+
return {
|
|
757
|
+
label,
|
|
758
|
+
productId: filters.productId ?? null,
|
|
759
|
+
totalDarkPurchases: 0,
|
|
760
|
+
totalDarkRevenue: 0,
|
|
761
|
+
exactPurchaseLinkedPurchases: 0,
|
|
762
|
+
exactPurchaseLinkedRevenue: 0,
|
|
763
|
+
userLinkedPurchases: 0,
|
|
764
|
+
userLinkedRevenue: 0,
|
|
765
|
+
byAnswer: [],
|
|
766
|
+
notes: [
|
|
767
|
+
"questionResponse table was not provided to this provider"
|
|
768
|
+
]
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const since = rangeToDate(range);
|
|
772
|
+
const purchaseConditions = [
|
|
773
|
+
paidPurchase(),
|
|
774
|
+
sql`NOT ${attributionSignalSql}`
|
|
775
|
+
];
|
|
776
|
+
if (since)
|
|
777
|
+
purchaseConditions.push(gte(purchases.createdAt, since));
|
|
778
|
+
if (filters.productId) {
|
|
779
|
+
purchaseConditions.push(eq(purchases.productId, filters.productId));
|
|
780
|
+
}
|
|
781
|
+
const darkRows = await db.select({
|
|
782
|
+
id: purchases.id,
|
|
783
|
+
userId: purchases.userId,
|
|
784
|
+
revenue: purchases.totalAmount
|
|
785
|
+
}).from(purchases).where(and(...purchaseConditions));
|
|
786
|
+
const totalDarkRevenue = darkRows.reduce((total, row) => total + Number(row.revenue ?? 0), 0);
|
|
787
|
+
if (darkRows.length === 0) {
|
|
788
|
+
return {
|
|
789
|
+
label,
|
|
790
|
+
productId: filters.productId ?? null,
|
|
791
|
+
totalDarkPurchases: 0,
|
|
792
|
+
totalDarkRevenue: 0,
|
|
793
|
+
exactPurchaseLinkedPurchases: 0,
|
|
794
|
+
exactPurchaseLinkedRevenue: 0,
|
|
795
|
+
userLinkedPurchases: 0,
|
|
796
|
+
userLinkedRevenue: 0,
|
|
797
|
+
byAnswer: [],
|
|
798
|
+
notes: [
|
|
799
|
+
"No dark paid purchases matched the filters"
|
|
800
|
+
]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const darkById = new Map(darkRows.map((row) => [
|
|
804
|
+
row.id,
|
|
805
|
+
row
|
|
806
|
+
]));
|
|
807
|
+
const darkByUserId = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const row of darkRows) {
|
|
809
|
+
if (!row.userId)
|
|
810
|
+
continue;
|
|
811
|
+
const bucket = darkByUserId.get(row.userId) ?? [];
|
|
812
|
+
bucket.push(row);
|
|
813
|
+
darkByUserId.set(row.userId, bucket);
|
|
814
|
+
}
|
|
815
|
+
const responseConditions = [];
|
|
816
|
+
if (since)
|
|
817
|
+
responseConditions.push(gte(questionResponse.createdAt, since));
|
|
818
|
+
const darkIds = [
|
|
819
|
+
...darkById.keys()
|
|
820
|
+
];
|
|
821
|
+
const darkUserIds = [
|
|
822
|
+
...darkByUserId.keys()
|
|
823
|
+
];
|
|
824
|
+
const exactPurchaseCondition = sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId')) IN (${sql.join(darkIds.map((id) => sql`${id}`), sql`, `)})`;
|
|
825
|
+
const userCondition = darkUserIds.length > 0 ? sql`${questionResponse.userId} IN (${sql.join(darkUserIds.map((id) => sql`${id}`), sql`, `)})` : sql`FALSE`;
|
|
826
|
+
responseConditions.push(sql`(${exactPurchaseCondition} OR ${userCondition})`);
|
|
827
|
+
const responseRows = await db.select({
|
|
828
|
+
purchaseId: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId'))`,
|
|
829
|
+
answer: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
|
|
830
|
+
userId: questionResponse.userId,
|
|
831
|
+
createdAt: questionResponse.createdAt
|
|
832
|
+
}).from(questionResponse).where(and(...responseConditions));
|
|
833
|
+
const exactByPurchase = /* @__PURE__ */ new Map();
|
|
834
|
+
const latestByUser = /* @__PURE__ */ new Map();
|
|
835
|
+
for (const row of responseRows) {
|
|
836
|
+
if (row.purchaseId && darkById.has(row.purchaseId)) {
|
|
837
|
+
exactByPurchase.set(row.purchaseId, row);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (!row.userId)
|
|
841
|
+
continue;
|
|
842
|
+
const current = latestByUser.get(row.userId);
|
|
843
|
+
const rowTime = new Date(row.createdAt ?? 0).getTime();
|
|
844
|
+
const currentTime = current ? new Date(current.createdAt ?? 0).getTime() : -1;
|
|
845
|
+
if (!current || rowTime >= currentTime)
|
|
846
|
+
latestByUser.set(row.userId, row);
|
|
847
|
+
}
|
|
848
|
+
const classified = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const [purchaseId, response] of exactByPurchase) {
|
|
850
|
+
const purchase = darkById.get(purchaseId);
|
|
851
|
+
classified.set(purchaseId, {
|
|
852
|
+
confidence: "exact_purchase_link",
|
|
853
|
+
answer: response.answer || "(no answer)",
|
|
854
|
+
revenue: Number(purchase?.revenue ?? 0)
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
for (const row of darkRows) {
|
|
858
|
+
if (classified.has(row.id) || !row.userId)
|
|
859
|
+
continue;
|
|
860
|
+
const response = latestByUser.get(row.userId);
|
|
861
|
+
if (!response)
|
|
862
|
+
continue;
|
|
863
|
+
classified.set(row.id, {
|
|
864
|
+
confidence: "user_linked",
|
|
865
|
+
answer: response.answer || "(no answer)",
|
|
866
|
+
revenue: Number(row.revenue ?? 0)
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
const byAnswer = /* @__PURE__ */ new Map();
|
|
870
|
+
let exactPurchaseLinkedPurchases = 0;
|
|
871
|
+
let exactPurchaseLinkedRevenue = 0;
|
|
872
|
+
let userLinkedPurchases = 0;
|
|
873
|
+
let userLinkedRevenue = 0;
|
|
874
|
+
for (const entry of classified.values()) {
|
|
875
|
+
if (entry.confidence === "exact_purchase_link") {
|
|
876
|
+
exactPurchaseLinkedPurchases += 1;
|
|
877
|
+
exactPurchaseLinkedRevenue += entry.revenue;
|
|
878
|
+
} else {
|
|
879
|
+
userLinkedPurchases += 1;
|
|
880
|
+
userLinkedRevenue += entry.revenue;
|
|
881
|
+
}
|
|
882
|
+
const key = `${entry.confidence}:${entry.answer}`;
|
|
883
|
+
const current = byAnswer.get(key) ?? {
|
|
884
|
+
answer: entry.answer,
|
|
885
|
+
confidence: entry.confidence,
|
|
886
|
+
purchases: 0,
|
|
887
|
+
revenue: 0
|
|
888
|
+
};
|
|
889
|
+
current.purchases += 1;
|
|
890
|
+
current.revenue += entry.revenue;
|
|
891
|
+
byAnswer.set(key, current);
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
label,
|
|
895
|
+
productId: filters.productId ?? null,
|
|
896
|
+
totalDarkPurchases: darkRows.length,
|
|
897
|
+
totalDarkRevenue,
|
|
898
|
+
exactPurchaseLinkedPurchases,
|
|
899
|
+
exactPurchaseLinkedRevenue,
|
|
900
|
+
userLinkedPurchases,
|
|
901
|
+
userLinkedRevenue,
|
|
902
|
+
byAnswer: [
|
|
903
|
+
...byAnswer.values()
|
|
904
|
+
].sort((a, b) => b.revenue - a.revenue || b.purchases - a.purchases).slice(0, limit),
|
|
905
|
+
notes: [
|
|
906
|
+
"Report-only fallback. No purchase fields are mutated.",
|
|
907
|
+
"Exact purchase-linked responses use QuestionResponse.fields.purchaseId when present.",
|
|
908
|
+
"User-linked responses are lower-confidence and remain report-only."
|
|
909
|
+
]
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
__name(getCheckoutSurveyFallbackReport, "getCheckoutSurveyFallbackReport");
|
|
913
|
+
async function getValuePathSummary(range = "30d") {
|
|
914
|
+
if (!contactEvent || !sideEffectIntent) {
|
|
915
|
+
return {
|
|
916
|
+
contacts: 0,
|
|
917
|
+
events: 0,
|
|
918
|
+
intents: 0,
|
|
919
|
+
completedIntents: 0,
|
|
920
|
+
pendingIntents: 0,
|
|
921
|
+
blockedIntents: 0,
|
|
922
|
+
answerEvents: 0,
|
|
923
|
+
dripEvents: 0,
|
|
924
|
+
enteredEvents: 0,
|
|
925
|
+
participantsWithAnswerClicks: 0,
|
|
926
|
+
participantsWithNoAnswerClicks: 0,
|
|
927
|
+
terminalParticipants: 0,
|
|
928
|
+
answerOptions: [],
|
|
929
|
+
answerSteps: [],
|
|
930
|
+
terminalSteps: [],
|
|
931
|
+
notes: [
|
|
932
|
+
"Contact Event and SideEffectIntent tables are unavailable."
|
|
933
|
+
]
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const since = rangeToDate(range);
|
|
937
|
+
const eventConditions = [
|
|
938
|
+
sql`${contactEvent.eventType} LIKE 'value-path.%'`
|
|
939
|
+
];
|
|
940
|
+
const intentConditions = [
|
|
941
|
+
eq(sideEffectIntent.type, "send-value-path-email")
|
|
942
|
+
];
|
|
943
|
+
if (since) {
|
|
944
|
+
eventConditions.push(gte(contactEvent.occurredAt, since));
|
|
945
|
+
intentConditions.push(gte(sideEffectIntent.createdAt, since));
|
|
946
|
+
}
|
|
947
|
+
const [events, intents] = await Promise.all([
|
|
948
|
+
db.select({
|
|
949
|
+
contactId: contactEvent.contactId,
|
|
950
|
+
eventType: contactEvent.eventType,
|
|
951
|
+
providerEventId: contactEvent.providerEventId,
|
|
952
|
+
occurredAt: contactEvent.occurredAt
|
|
953
|
+
}).from(contactEvent).where(and(...eventConditions)),
|
|
954
|
+
db.select({
|
|
955
|
+
contactId: sideEffectIntent.contactId,
|
|
956
|
+
status: sideEffectIntent.status,
|
|
957
|
+
metadata: sideEffectIntent.metadata,
|
|
958
|
+
createdAt: sideEffectIntent.createdAt
|
|
959
|
+
}).from(sideEffectIntent).where(and(...intentConditions))
|
|
960
|
+
]);
|
|
961
|
+
const contactIds = /* @__PURE__ */ new Set();
|
|
962
|
+
for (const event of events)
|
|
963
|
+
contactIds.add(event.contactId);
|
|
964
|
+
for (const intent of intents)
|
|
965
|
+
contactIds.add(intent.contactId);
|
|
966
|
+
const answerEvents = events.filter((event) => event.eventType === "value-path.answer-selected");
|
|
967
|
+
const dripEvents = events.filter((event) => event.eventType === "value-path.drip-progressed");
|
|
968
|
+
const enteredEvents = events.filter((event) => event.eventType === "value-path.entered");
|
|
969
|
+
const answersByContact = /* @__PURE__ */ new Map();
|
|
970
|
+
for (const event of answerEvents) {
|
|
971
|
+
answersByContact.set(event.contactId, (answersByContact.get(event.contactId) ?? 0) + 1);
|
|
972
|
+
}
|
|
973
|
+
const answerKeyCounts = /* @__PURE__ */ new Map();
|
|
974
|
+
const answerStepCounts = /* @__PURE__ */ new Map();
|
|
975
|
+
for (const event of answerEvents) {
|
|
976
|
+
const key = parseValuePathAnswerKey(event.providerEventId);
|
|
977
|
+
answerKeyCounts.set(key, (answerKeyCounts.get(key) ?? 0) + 1);
|
|
978
|
+
const step = answerStepFromKey(key);
|
|
979
|
+
answerStepCounts.set(step, (answerStepCounts.get(step) ?? 0) + 1);
|
|
980
|
+
}
|
|
981
|
+
const latestIntentByContact = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const intent of [
|
|
983
|
+
...intents
|
|
984
|
+
].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())) {
|
|
985
|
+
latestIntentByContact.set(intent.contactId, intent);
|
|
986
|
+
}
|
|
987
|
+
const terminalCounts = /* @__PURE__ */ new Map();
|
|
988
|
+
for (const intent of latestIntentByContact.values()) {
|
|
989
|
+
const emailResourceId = String(intent.metadata?.emailResourceId ?? "");
|
|
990
|
+
if (!isTerminalValuePathEmail(emailResourceId))
|
|
991
|
+
continue;
|
|
992
|
+
terminalCounts.set(emailResourceId, (terminalCounts.get(emailResourceId) ?? 0) + 1);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
contacts: contactIds.size,
|
|
996
|
+
events: events.length,
|
|
997
|
+
intents: intents.length,
|
|
998
|
+
completedIntents: intents.filter((intent) => intent.status === "completed").length,
|
|
999
|
+
pendingIntents: intents.filter((intent) => intent.status === "pending").length,
|
|
1000
|
+
blockedIntents: intents.filter((intent) => intent.status === "blocked").length,
|
|
1001
|
+
answerEvents: answerEvents.length,
|
|
1002
|
+
dripEvents: dripEvents.length,
|
|
1003
|
+
enteredEvents: enteredEvents.length,
|
|
1004
|
+
participantsWithAnswerClicks: answersByContact.size,
|
|
1005
|
+
participantsWithNoAnswerClicks: Math.max(0, contactIds.size - answersByContact.size),
|
|
1006
|
+
terminalParticipants: [
|
|
1007
|
+
...terminalCounts.values()
|
|
1008
|
+
].reduce((total, count3) => total + count3, 0),
|
|
1009
|
+
answerOptions: [
|
|
1010
|
+
...answerKeyCounts.entries()
|
|
1011
|
+
].map(([key, count3]) => ({
|
|
1012
|
+
key,
|
|
1013
|
+
step: answerStepFromKey(key),
|
|
1014
|
+
optionValue: answerOptionFromKey(key),
|
|
1015
|
+
count: count3
|
|
1016
|
+
})).sort((a, b) => b.count - a.count),
|
|
1017
|
+
answerSteps: [
|
|
1018
|
+
...answerStepCounts.entries()
|
|
1019
|
+
].map(([step, count3]) => ({
|
|
1020
|
+
step,
|
|
1021
|
+
count: count3
|
|
1022
|
+
})).sort((a, b) => b.count - a.count),
|
|
1023
|
+
terminalSteps: [
|
|
1024
|
+
...terminalCounts.entries()
|
|
1025
|
+
].map(([emailResourceId, count3]) => ({
|
|
1026
|
+
emailResourceId,
|
|
1027
|
+
count: count3
|
|
1028
|
+
})).sort((a, b) => b.count - a.count),
|
|
1029
|
+
notes: [
|
|
1030
|
+
"Counts are based on Contact Events and SideEffectIntents.",
|
|
1031
|
+
"Kit-native opens, unsubscribes, complaints, and raw Kit link clicks are not included."
|
|
1032
|
+
]
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
__name(getValuePathSummary, "getValuePathSummary");
|
|
1036
|
+
function parseValuePathAnswerKey(providerEventId) {
|
|
1037
|
+
const match = String(providerEventId ?? "").match(/:answer:(.+)$/);
|
|
1038
|
+
return match?.[1] ?? "unknown";
|
|
1039
|
+
}
|
|
1040
|
+
__name(parseValuePathAnswerKey, "parseValuePathAnswerKey");
|
|
1041
|
+
function answerStepFromKey(key) {
|
|
1042
|
+
const index = key.lastIndexOf(".");
|
|
1043
|
+
return index > -1 ? key.slice(0, index) : key;
|
|
1044
|
+
}
|
|
1045
|
+
__name(answerStepFromKey, "answerStepFromKey");
|
|
1046
|
+
function answerOptionFromKey(key) {
|
|
1047
|
+
const index = key.lastIndexOf(".");
|
|
1048
|
+
return index > -1 ? key.slice(index + 1) : "unknown";
|
|
1049
|
+
}
|
|
1050
|
+
__name(answerOptionFromKey, "answerOptionFromKey");
|
|
1051
|
+
function isTerminalValuePathEmail(emailResourceId) {
|
|
1052
|
+
return /(?:^|\.)(?:email-6|team-email-6)$/.test(emailResourceId);
|
|
1053
|
+
}
|
|
1054
|
+
__name(isTerminalValuePathEmail, "isTerminalValuePathEmail");
|
|
368
1055
|
async function traceAttribution(opts) {
|
|
369
1056
|
const events = [];
|
|
370
1057
|
let userId = null;
|
|
@@ -521,8 +1208,12 @@ function createDatabaseProvider(db, schema) {
|
|
|
521
1208
|
getShortlinkPerformance,
|
|
522
1209
|
getRevenueBySource,
|
|
523
1210
|
getConversionFunnel,
|
|
1211
|
+
getCommerceLaneSummary,
|
|
524
1212
|
getAttributedRevenueSummary,
|
|
525
1213
|
getContentPurchaseCorrelation,
|
|
1214
|
+
getCheckoutAttributionReceipt,
|
|
1215
|
+
getCheckoutSurveyFallbackReport,
|
|
1216
|
+
getValuePathSummary,
|
|
526
1217
|
traceAttribution
|
|
527
1218
|
};
|
|
528
1219
|
}
|
|
@@ -1202,9 +1893,9 @@ function createSurveyProvider(db, schema) {
|
|
|
1202
1893
|
}));
|
|
1203
1894
|
}
|
|
1204
1895
|
__name(getSurveyQuestionBreakdown, "getSurveyQuestionBreakdown");
|
|
1205
|
-
async function getSurveyResponses(range = "30d", limit = 100) {
|
|
1896
|
+
async function getSurveyResponses(range = "30d", limit = 100, offset = 0) {
|
|
1206
1897
|
const canonicalRows = await fetchCanonicalRows(range);
|
|
1207
|
-
return canonicalRows.slice(
|
|
1898
|
+
return canonicalRows.slice(offset, offset + limit).map((row) => ({
|
|
1208
1899
|
responseId: row.responseId,
|
|
1209
1900
|
surveyId: row.surveyId,
|
|
1210
1901
|
surveyTitle: row.surveyTitle ?? "",
|