@coursebuilder/analytics 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +29 -0
- package/dist/components/index.js +91 -2
- 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 +144 -2
- package/dist/providers/database.js +652 -20
- package/dist/providers/database.js.map +1 -1
- package/dist/providers/index.js +654 -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 +151 -3
- package/package.json +5 -3
- 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 +163 -0
- package/src/engine.ts +66 -6
- package/src/providers/attribution-recovery.test.ts +63 -0
- package/src/providers/attribution-recovery.ts +97 -0
- package/src/providers/database.ts +812 -42
- package/src/providers/survey.ts +3 -1
- package/src/types.ts +166 -2
|
@@ -3,18 +3,64 @@ 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
|
+
)`;
|
|
18
64
|
function rangeToDate(range) {
|
|
19
65
|
if (range === "all")
|
|
20
66
|
return null;
|
|
@@ -255,21 +301,80 @@ function createDatabaseProvider(db, schema) {
|
|
|
255
301
|
});
|
|
256
302
|
}
|
|
257
303
|
__name(getShortlinkPerformance, "getShortlinkPerformance");
|
|
258
|
-
async function getRevenueBySource(range = "30d") {
|
|
304
|
+
async function getRevenueBySource(range = "30d", filters = {}) {
|
|
259
305
|
const since = rangeToDate(range);
|
|
260
306
|
const conditions = [
|
|
261
|
-
|
|
307
|
+
commerceRecord()
|
|
262
308
|
];
|
|
263
309
|
if (since)
|
|
264
310
|
conditions.push(gte(purchases.createdAt, since));
|
|
311
|
+
if (filters.productId)
|
|
312
|
+
conditions.push(eq(purchases.productId, filters.productId));
|
|
313
|
+
const kindExpr = sql`CASE
|
|
314
|
+
WHEN ${syntheticSignalSql} THEN 'synthetic'
|
|
315
|
+
WHEN ${purchases.totalAmount} = 0 THEN 'access_grant'
|
|
316
|
+
WHEN ${purchases.totalAmount} > 0 THEN 'paid_conversion'
|
|
317
|
+
ELSE 'unknown'
|
|
318
|
+
END`;
|
|
319
|
+
const sourceExpr = sql`CASE
|
|
320
|
+
WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
|
|
321
|
+
THEN 'internal'
|
|
322
|
+
WHEN ${shortlinkRecoveredAttributionSignalSql} THEN ${SHORTLINK_RECOVERY_LANE}
|
|
323
|
+
ELSE COALESCE(
|
|
324
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.source')), 'null'),
|
|
325
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.source')), 'null'),
|
|
326
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')), 'null'),
|
|
327
|
+
CASE
|
|
328
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
|
|
329
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'self_reported'
|
|
330
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'ga4'
|
|
331
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'ga4'
|
|
332
|
+
ELSE NULL
|
|
333
|
+
END
|
|
334
|
+
)
|
|
335
|
+
END`;
|
|
336
|
+
const mediumExpr = sql`CASE
|
|
337
|
+
WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
|
|
338
|
+
THEN 'free_upgrade'
|
|
339
|
+
WHEN ${shortlinkRecoveredAttributionSignalSql} THEN 'shortlink'
|
|
340
|
+
ELSE COALESCE(
|
|
341
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.medium')), 'null'),
|
|
342
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.medium')), 'null'),
|
|
343
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmMedium')), 'null'),
|
|
344
|
+
CASE
|
|
345
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL THEN 'shortlink'
|
|
346
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL THEN 'checkout_survey'
|
|
347
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL THEN 'client_id'
|
|
348
|
+
WHEN JSON_EXTRACT(${purchases.fields}, '$.gaClientId') IS NOT NULL THEN 'client_id'
|
|
349
|
+
ELSE NULL
|
|
350
|
+
END
|
|
351
|
+
)
|
|
352
|
+
END`;
|
|
353
|
+
const campaignExpr = sql`CASE
|
|
354
|
+
WHEN JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfill')) = 'true'
|
|
355
|
+
THEN COALESCE(
|
|
356
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.migrationBatchId')), 'null'),
|
|
357
|
+
'free_upgrade'
|
|
358
|
+
)
|
|
359
|
+
WHEN ${shortlinkRecoveredAttributionSignalSql} THEN NULL
|
|
360
|
+
ELSE COALESCE(
|
|
361
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.campaign')), 'null'),
|
|
362
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.utm.campaign')), 'null'),
|
|
363
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmCampaign')), 'null'),
|
|
364
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug')), 'null'),
|
|
365
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource')), 'null')
|
|
366
|
+
)
|
|
367
|
+
END`;
|
|
265
368
|
const rows = await db.select({
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
369
|
+
kind: kindExpr.as("kind"),
|
|
370
|
+
source: sourceExpr.as("source"),
|
|
371
|
+
medium: mediumExpr.as("medium"),
|
|
372
|
+
campaign: campaignExpr.as("campaign"),
|
|
269
373
|
revenue: sum(purchases.totalAmount),
|
|
270
374
|
count: count()
|
|
271
|
-
}).from(purchases).where(and(...conditions)).groupBy(sql`
|
|
375
|
+
}).from(purchases).where(and(...conditions)).groupBy(sql`1`, sql`2`, sql`3`, sql`4`).orderBy(desc(sum(purchases.totalAmount)), desc(count()));
|
|
272
376
|
return rows.map((r) => ({
|
|
377
|
+
kind: r.kind ?? "unknown",
|
|
273
378
|
source: r.source ?? null,
|
|
274
379
|
medium: r.medium ?? null,
|
|
275
380
|
campaign: r.campaign ?? null,
|
|
@@ -296,10 +401,7 @@ function createDatabaseProvider(db, schema) {
|
|
|
296
401
|
}).from(purchases).where(and(...purchaseConditions));
|
|
297
402
|
const [attributedCount] = await db.select({
|
|
298
403
|
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
|
-
)`));
|
|
404
|
+
}).from(purchases).where(and(...purchaseConditions, attributionSignalSql));
|
|
303
405
|
const totalSignups = userCount?.total ?? 0;
|
|
304
406
|
const totalPurchases = purchaseCount?.total ?? 0;
|
|
305
407
|
const attributedPurchases = attributedCount?.total ?? 0;
|
|
@@ -312,33 +414,146 @@ function createDatabaseProvider(db, schema) {
|
|
|
312
414
|
};
|
|
313
415
|
}
|
|
314
416
|
__name(getConversionFunnel, "getConversionFunnel");
|
|
315
|
-
async function
|
|
417
|
+
async function getCommerceLaneSummary(range = "30d", filters = {}) {
|
|
418
|
+
const since = rangeToDate(range);
|
|
419
|
+
const conditions = [
|
|
420
|
+
commerceRecord()
|
|
421
|
+
];
|
|
422
|
+
if (since)
|
|
423
|
+
conditions.push(gte(purchases.createdAt, since));
|
|
424
|
+
if (filters.productId)
|
|
425
|
+
conditions.push(eq(purchases.productId, filters.productId));
|
|
426
|
+
const [commerce] = await db.select({
|
|
427
|
+
count: count()
|
|
428
|
+
}).from(purchases).where(and(...conditions));
|
|
429
|
+
const [paid] = await db.select({
|
|
430
|
+
count: count(),
|
|
431
|
+
revenue: sum(purchases.totalAmount)
|
|
432
|
+
}).from(purchases).where(and(...conditions, paidPurchase()));
|
|
433
|
+
const [grants] = await db.select({
|
|
434
|
+
count: count(),
|
|
435
|
+
revenue: sum(purchases.totalAmount)
|
|
436
|
+
}).from(purchases).where(and(...conditions, accessGrant()));
|
|
437
|
+
const [freeUpgrades] = await db.select({
|
|
438
|
+
count: count()
|
|
439
|
+
}).from(purchases).where(and(...conditions, backfillFreeUpgrade()));
|
|
440
|
+
const [synthetic] = await db.select({
|
|
441
|
+
count: count()
|
|
442
|
+
}).from(purchases).where(and(...conditions, syntheticPurchase()));
|
|
443
|
+
const reasonRows = await db.select({
|
|
444
|
+
reason: sql`COALESCE(
|
|
445
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
|
|
446
|
+
'access_grant'
|
|
447
|
+
)`.as("reason"),
|
|
448
|
+
count: count()
|
|
449
|
+
}).from(purchases).where(and(...conditions, accessGrant())).groupBy(sql`COALESCE(
|
|
450
|
+
NULLIF(JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.backfillReason')), 'null'),
|
|
451
|
+
'access_grant'
|
|
452
|
+
)`);
|
|
453
|
+
return {
|
|
454
|
+
commerceRecords: commerce?.count ?? 0,
|
|
455
|
+
paidPurchases: paid?.count ?? 0,
|
|
456
|
+
paidRevenue: Number(paid?.revenue ?? 0),
|
|
457
|
+
accessGrants: grants?.count ?? 0,
|
|
458
|
+
accessGrantRevenue: Number(grants?.revenue ?? 0),
|
|
459
|
+
freeUpgrades: freeUpgrades?.count ?? 0,
|
|
460
|
+
syntheticPurchases: synthetic?.count ?? 0,
|
|
461
|
+
byAccessGrantReason: Object.fromEntries(reasonRows.map((row) => [
|
|
462
|
+
row.reason,
|
|
463
|
+
row.count
|
|
464
|
+
]))
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
__name(getCommerceLaneSummary, "getCommerceLaneSummary");
|
|
468
|
+
async function getAttributedRevenueSummary(range = "30d", filters = {}) {
|
|
316
469
|
const since = rangeToDate(range);
|
|
317
470
|
const conditions = [
|
|
318
471
|
paidPurchase()
|
|
319
472
|
];
|
|
320
473
|
if (since)
|
|
321
474
|
conditions.push(gte(purchases.createdAt, since));
|
|
475
|
+
if (filters.productId)
|
|
476
|
+
conditions.push(eq(purchases.productId, filters.productId));
|
|
322
477
|
const [totals] = await db.select({
|
|
323
478
|
total: sum(purchases.totalAmount),
|
|
324
479
|
count: count()
|
|
325
480
|
}).from(purchases).where(and(...conditions));
|
|
326
481
|
const [attributed] = await db.select({
|
|
327
|
-
total: sum(purchases.totalAmount)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
482
|
+
total: sum(purchases.totalAmount),
|
|
483
|
+
count: count()
|
|
484
|
+
}).from(purchases).where(and(...conditions, attributionSignalSql));
|
|
485
|
+
const [purchaseFieldAttributed] = await db.select({
|
|
486
|
+
total: sum(purchases.totalAmount),
|
|
487
|
+
count: count()
|
|
488
|
+
}).from(purchases).where(and(...conditions, purchaseFieldAttributionSignalSql));
|
|
489
|
+
const [recoveredFromShortlinkAttributionTable] = await db.select({
|
|
490
|
+
total: sum(purchases.totalAmount),
|
|
491
|
+
count: count()
|
|
492
|
+
}).from(purchases).where(and(...conditions, shortlinkRecoveredAttributionSignalSql));
|
|
493
|
+
const signalConditions = since ? [
|
|
494
|
+
commerceRecord(),
|
|
495
|
+
gte(purchases.createdAt, since)
|
|
496
|
+
] : [
|
|
497
|
+
commerceRecord()
|
|
498
|
+
];
|
|
499
|
+
if (filters.productId) {
|
|
500
|
+
signalConditions.push(eq(purchases.productId, filters.productId));
|
|
501
|
+
}
|
|
502
|
+
const signalCount = /* @__PURE__ */ __name(async (condition) => {
|
|
503
|
+
const [row] = await db.select({
|
|
504
|
+
count: count()
|
|
505
|
+
}).from(purchases).where(and(...signalConditions, condition));
|
|
506
|
+
return row?.count ?? 0;
|
|
507
|
+
}, "signalCount");
|
|
508
|
+
const [shortlinkCount, utmCount, clickIdCount, gaClientIdCount, selfReportedSourceCount, recoveredFromShortlinkAttributionTableCount] = await Promise.all([
|
|
509
|
+
signalCount(sql`(
|
|
510
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink.slug') IS NOT NULL
|
|
511
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
|
|
512
|
+
)`),
|
|
513
|
+
signalCount(sql`(
|
|
514
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
|
|
515
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
|
|
516
|
+
)`),
|
|
517
|
+
signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL`),
|
|
518
|
+
signalCount(sql`(
|
|
519
|
+
JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
|
|
520
|
+
OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
|
|
521
|
+
)`),
|
|
522
|
+
signalCount(sql`JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL`),
|
|
523
|
+
signalCount(shortlinkRecoveredAttributionSignalSql)
|
|
524
|
+
]);
|
|
525
|
+
const commerce = await getCommerceLaneSummary(range, filters);
|
|
332
526
|
const totalRevenue = Number(totals?.total ?? 0);
|
|
333
527
|
const attributedRevenue = Number(attributed?.total ?? 0);
|
|
528
|
+
const purchaseFieldAttributedRevenue = Number(purchaseFieldAttributed?.total ?? 0);
|
|
529
|
+
const recoveredFromShortlinkAttributionTableRevenue = Number(recoveredFromShortlinkAttributionTable?.total ?? 0);
|
|
334
530
|
const unattributedRevenue = totalRevenue - attributedRevenue;
|
|
335
|
-
const
|
|
531
|
+
const paidPurchases = totals?.count ?? 0;
|
|
336
532
|
return {
|
|
337
533
|
totalRevenue,
|
|
338
534
|
attributedRevenue,
|
|
339
535
|
unattributedRevenue,
|
|
340
536
|
attributionRate: totalRevenue > 0 ? attributedRevenue / totalRevenue : 0,
|
|
341
|
-
totalPurchases
|
|
537
|
+
totalPurchases: commerce.commerceRecords,
|
|
538
|
+
paidPurchases,
|
|
539
|
+
purchaseFieldAttributedPurchases: purchaseFieldAttributed?.count ?? 0,
|
|
540
|
+
purchaseFieldAttributedRevenue,
|
|
541
|
+
recoveredFromShortlinkAttributionTablePurchases: recoveredFromShortlinkAttributionTable?.count ?? 0,
|
|
542
|
+
recoveredFromShortlinkAttributionTableRevenue,
|
|
543
|
+
commerceRecords: commerce.commerceRecords,
|
|
544
|
+
accessGrants: commerce.accessGrants,
|
|
545
|
+
freeUpgrades: commerce.freeUpgrades,
|
|
546
|
+
syntheticPurchases: commerce.syntheticPurchases,
|
|
547
|
+
commerce,
|
|
548
|
+
signals: {
|
|
549
|
+
shortlink: shortlinkCount,
|
|
550
|
+
utm: utmCount,
|
|
551
|
+
paidClickId: clickIdCount,
|
|
552
|
+
gaClientId: gaClientIdCount,
|
|
553
|
+
selfReportedSource: selfReportedSourceCount,
|
|
554
|
+
recoveredFromShortlinkAttributionTable: recoveredFromShortlinkAttributionTableCount,
|
|
555
|
+
internalFreeUpgrade: commerce.freeUpgrades
|
|
556
|
+
}
|
|
342
557
|
};
|
|
343
558
|
}
|
|
344
559
|
__name(getAttributedRevenueSummary, "getAttributedRevenueSummary");
|
|
@@ -365,6 +580,419 @@ function createDatabaseProvider(db, schema) {
|
|
|
365
580
|
}));
|
|
366
581
|
}
|
|
367
582
|
__name(getContentPurchaseCorrelation, "getContentPurchaseCorrelation");
|
|
583
|
+
function parseJsonRecord(value) {
|
|
584
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
585
|
+
}
|
|
586
|
+
__name(parseJsonRecord, "parseJsonRecord");
|
|
587
|
+
function hasRecordValue(value) {
|
|
588
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.length > 0);
|
|
589
|
+
}
|
|
590
|
+
__name(hasRecordValue, "hasRecordValue");
|
|
591
|
+
function hasSyntheticClickId(value) {
|
|
592
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).some((entry) => typeof entry === "string" && entry.startsWith("TEST_"));
|
|
593
|
+
}
|
|
594
|
+
__name(hasSyntheticClickId, "hasSyntheticClickId");
|
|
595
|
+
async function getCheckoutAttributionReceipt(opts) {
|
|
596
|
+
if (!opts.purchaseId) {
|
|
597
|
+
return {
|
|
598
|
+
purchase: null,
|
|
599
|
+
checks: {
|
|
600
|
+
purchaseFound: false,
|
|
601
|
+
attributionSnapshot: false,
|
|
602
|
+
utm: false,
|
|
603
|
+
actualUtm: false,
|
|
604
|
+
clickId: false,
|
|
605
|
+
synthetic: false,
|
|
606
|
+
shortlink: false,
|
|
607
|
+
selfReportedSource: false,
|
|
608
|
+
gaClientId: false
|
|
609
|
+
},
|
|
610
|
+
attribution: null,
|
|
611
|
+
legacy: {},
|
|
612
|
+
fieldKeys: []
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const [row] = await db.select({
|
|
616
|
+
id: purchases.id,
|
|
617
|
+
createdAt: purchases.createdAt,
|
|
618
|
+
productId: purchases.productId,
|
|
619
|
+
productName: products.name,
|
|
620
|
+
totalAmount: purchases.totalAmount,
|
|
621
|
+
status: purchases.status,
|
|
622
|
+
country: purchases.country,
|
|
623
|
+
fields: purchases.fields
|
|
624
|
+
}).from(purchases).leftJoin(products, eq(purchases.productId, products.id)).where(eq(purchases.id, opts.purchaseId)).limit(1);
|
|
625
|
+
if (!row) {
|
|
626
|
+
return {
|
|
627
|
+
purchase: null,
|
|
628
|
+
checks: {
|
|
629
|
+
purchaseFound: false,
|
|
630
|
+
attributionSnapshot: false,
|
|
631
|
+
utm: false,
|
|
632
|
+
actualUtm: false,
|
|
633
|
+
clickId: false,
|
|
634
|
+
synthetic: false,
|
|
635
|
+
shortlink: false,
|
|
636
|
+
selfReportedSource: false,
|
|
637
|
+
gaClientId: false
|
|
638
|
+
},
|
|
639
|
+
attribution: null,
|
|
640
|
+
legacy: {},
|
|
641
|
+
fieldKeys: []
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const fields = parseJsonRecord(row.fields);
|
|
645
|
+
const attribution = parseJsonRecord(fields.attribution);
|
|
646
|
+
const utm = parseJsonRecord(attribution.utm);
|
|
647
|
+
const clickIds = parseJsonRecord(attribution.clickIds);
|
|
648
|
+
const shortlinkSnapshot = parseJsonRecord(attribution.shortlink);
|
|
649
|
+
const ga = parseJsonRecord(attribution.ga);
|
|
650
|
+
const legacy = Object.fromEntries([
|
|
651
|
+
"utmSource",
|
|
652
|
+
"utmMedium",
|
|
653
|
+
"utmCampaign",
|
|
654
|
+
"gaClientId",
|
|
655
|
+
"shortlinkRef",
|
|
656
|
+
"landingPath",
|
|
657
|
+
"referrer",
|
|
658
|
+
"ltUtmSource",
|
|
659
|
+
"ltUtmMedium",
|
|
660
|
+
"ltUtmCampaign"
|
|
661
|
+
].map((key) => [
|
|
662
|
+
key,
|
|
663
|
+
fields[key]
|
|
664
|
+
]).filter(([, value]) => value !== void 0 && value !== null));
|
|
665
|
+
const hasActualUtm = Boolean(utm.source || utm.medium || utm.campaign || fields.utmSource);
|
|
666
|
+
const synthetic = Boolean(attribution.synthetic || fields.synthetic || hasSyntheticClickId(clickIds));
|
|
667
|
+
return {
|
|
668
|
+
purchase: {
|
|
669
|
+
id: row.id,
|
|
670
|
+
createdAt: row.createdAt,
|
|
671
|
+
productId: row.productId,
|
|
672
|
+
productName: row.productName ?? null,
|
|
673
|
+
totalAmount: Number(row.totalAmount ?? 0),
|
|
674
|
+
status: row.status,
|
|
675
|
+
country: row.country
|
|
676
|
+
},
|
|
677
|
+
checks: {
|
|
678
|
+
purchaseFound: true,
|
|
679
|
+
attributionSnapshot: Object.keys(attribution).length > 0,
|
|
680
|
+
utm: hasActualUtm,
|
|
681
|
+
actualUtm: hasActualUtm,
|
|
682
|
+
clickId: hasRecordValue(clickIds),
|
|
683
|
+
synthetic,
|
|
684
|
+
shortlink: Boolean(shortlinkSnapshot.slug || fields.shortlinkRef),
|
|
685
|
+
selfReportedSource: Boolean(attribution.selfReportedSource),
|
|
686
|
+
gaClientId: Boolean(ga.clientId || fields.gaClientId)
|
|
687
|
+
},
|
|
688
|
+
attribution: Object.keys(attribution).length > 0 ? attribution : null,
|
|
689
|
+
legacy,
|
|
690
|
+
fieldKeys: Object.keys(fields).sort()
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
__name(getCheckoutAttributionReceipt, "getCheckoutAttributionReceipt");
|
|
694
|
+
async function getCheckoutSurveyFallbackReport(range = "30d", limit = 20, filters = {}) {
|
|
695
|
+
const label = "recovered_from_checkout_survey_response";
|
|
696
|
+
if (!questionResponse) {
|
|
697
|
+
return {
|
|
698
|
+
label,
|
|
699
|
+
productId: filters.productId ?? null,
|
|
700
|
+
totalDarkPurchases: 0,
|
|
701
|
+
totalDarkRevenue: 0,
|
|
702
|
+
exactPurchaseLinkedPurchases: 0,
|
|
703
|
+
exactPurchaseLinkedRevenue: 0,
|
|
704
|
+
userLinkedPurchases: 0,
|
|
705
|
+
userLinkedRevenue: 0,
|
|
706
|
+
byAnswer: [],
|
|
707
|
+
notes: [
|
|
708
|
+
"questionResponse table was not provided to this provider"
|
|
709
|
+
]
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const since = rangeToDate(range);
|
|
713
|
+
const purchaseConditions = [
|
|
714
|
+
paidPurchase(),
|
|
715
|
+
sql`NOT ${attributionSignalSql}`
|
|
716
|
+
];
|
|
717
|
+
if (since)
|
|
718
|
+
purchaseConditions.push(gte(purchases.createdAt, since));
|
|
719
|
+
if (filters.productId) {
|
|
720
|
+
purchaseConditions.push(eq(purchases.productId, filters.productId));
|
|
721
|
+
}
|
|
722
|
+
const darkRows = await db.select({
|
|
723
|
+
id: purchases.id,
|
|
724
|
+
userId: purchases.userId,
|
|
725
|
+
revenue: purchases.totalAmount
|
|
726
|
+
}).from(purchases).where(and(...purchaseConditions));
|
|
727
|
+
const totalDarkRevenue = darkRows.reduce((total, row) => total + Number(row.revenue ?? 0), 0);
|
|
728
|
+
if (darkRows.length === 0) {
|
|
729
|
+
return {
|
|
730
|
+
label,
|
|
731
|
+
productId: filters.productId ?? null,
|
|
732
|
+
totalDarkPurchases: 0,
|
|
733
|
+
totalDarkRevenue: 0,
|
|
734
|
+
exactPurchaseLinkedPurchases: 0,
|
|
735
|
+
exactPurchaseLinkedRevenue: 0,
|
|
736
|
+
userLinkedPurchases: 0,
|
|
737
|
+
userLinkedRevenue: 0,
|
|
738
|
+
byAnswer: [],
|
|
739
|
+
notes: [
|
|
740
|
+
"No dark paid purchases matched the filters"
|
|
741
|
+
]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const darkById = new Map(darkRows.map((row) => [
|
|
745
|
+
row.id,
|
|
746
|
+
row
|
|
747
|
+
]));
|
|
748
|
+
const darkByUserId = /* @__PURE__ */ new Map();
|
|
749
|
+
for (const row of darkRows) {
|
|
750
|
+
if (!row.userId)
|
|
751
|
+
continue;
|
|
752
|
+
const bucket = darkByUserId.get(row.userId) ?? [];
|
|
753
|
+
bucket.push(row);
|
|
754
|
+
darkByUserId.set(row.userId, bucket);
|
|
755
|
+
}
|
|
756
|
+
const responseConditions = [];
|
|
757
|
+
if (since)
|
|
758
|
+
responseConditions.push(gte(questionResponse.createdAt, since));
|
|
759
|
+
const darkIds = [
|
|
760
|
+
...darkById.keys()
|
|
761
|
+
];
|
|
762
|
+
const darkUserIds = [
|
|
763
|
+
...darkByUserId.keys()
|
|
764
|
+
];
|
|
765
|
+
const exactPurchaseCondition = sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId')) IN (${sql.join(darkIds.map((id) => sql`${id}`), sql`, `)})`;
|
|
766
|
+
const userCondition = darkUserIds.length > 0 ? sql`${questionResponse.userId} IN (${sql.join(darkUserIds.map((id) => sql`${id}`), sql`, `)})` : sql`FALSE`;
|
|
767
|
+
responseConditions.push(sql`(${exactPurchaseCondition} OR ${userCondition})`);
|
|
768
|
+
const responseRows = await db.select({
|
|
769
|
+
purchaseId: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.purchaseId'))`,
|
|
770
|
+
answer: sql`JSON_UNQUOTE(JSON_EXTRACT(${questionResponse.fields}, '$.answer'))`,
|
|
771
|
+
userId: questionResponse.userId,
|
|
772
|
+
createdAt: questionResponse.createdAt
|
|
773
|
+
}).from(questionResponse).where(and(...responseConditions));
|
|
774
|
+
const exactByPurchase = /* @__PURE__ */ new Map();
|
|
775
|
+
const latestByUser = /* @__PURE__ */ new Map();
|
|
776
|
+
for (const row of responseRows) {
|
|
777
|
+
if (row.purchaseId && darkById.has(row.purchaseId)) {
|
|
778
|
+
exactByPurchase.set(row.purchaseId, row);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (!row.userId)
|
|
782
|
+
continue;
|
|
783
|
+
const current = latestByUser.get(row.userId);
|
|
784
|
+
const rowTime = new Date(row.createdAt ?? 0).getTime();
|
|
785
|
+
const currentTime = current ? new Date(current.createdAt ?? 0).getTime() : -1;
|
|
786
|
+
if (!current || rowTime >= currentTime)
|
|
787
|
+
latestByUser.set(row.userId, row);
|
|
788
|
+
}
|
|
789
|
+
const classified = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const [purchaseId, response] of exactByPurchase) {
|
|
791
|
+
const purchase = darkById.get(purchaseId);
|
|
792
|
+
classified.set(purchaseId, {
|
|
793
|
+
confidence: "exact_purchase_link",
|
|
794
|
+
answer: response.answer || "(no answer)",
|
|
795
|
+
revenue: Number(purchase?.revenue ?? 0)
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
for (const row of darkRows) {
|
|
799
|
+
if (classified.has(row.id) || !row.userId)
|
|
800
|
+
continue;
|
|
801
|
+
const response = latestByUser.get(row.userId);
|
|
802
|
+
if (!response)
|
|
803
|
+
continue;
|
|
804
|
+
classified.set(row.id, {
|
|
805
|
+
confidence: "user_linked",
|
|
806
|
+
answer: response.answer || "(no answer)",
|
|
807
|
+
revenue: Number(row.revenue ?? 0)
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
const byAnswer = /* @__PURE__ */ new Map();
|
|
811
|
+
let exactPurchaseLinkedPurchases = 0;
|
|
812
|
+
let exactPurchaseLinkedRevenue = 0;
|
|
813
|
+
let userLinkedPurchases = 0;
|
|
814
|
+
let userLinkedRevenue = 0;
|
|
815
|
+
for (const entry of classified.values()) {
|
|
816
|
+
if (entry.confidence === "exact_purchase_link") {
|
|
817
|
+
exactPurchaseLinkedPurchases += 1;
|
|
818
|
+
exactPurchaseLinkedRevenue += entry.revenue;
|
|
819
|
+
} else {
|
|
820
|
+
userLinkedPurchases += 1;
|
|
821
|
+
userLinkedRevenue += entry.revenue;
|
|
822
|
+
}
|
|
823
|
+
const key = `${entry.confidence}:${entry.answer}`;
|
|
824
|
+
const current = byAnswer.get(key) ?? {
|
|
825
|
+
answer: entry.answer,
|
|
826
|
+
confidence: entry.confidence,
|
|
827
|
+
purchases: 0,
|
|
828
|
+
revenue: 0
|
|
829
|
+
};
|
|
830
|
+
current.purchases += 1;
|
|
831
|
+
current.revenue += entry.revenue;
|
|
832
|
+
byAnswer.set(key, current);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
label,
|
|
836
|
+
productId: filters.productId ?? null,
|
|
837
|
+
totalDarkPurchases: darkRows.length,
|
|
838
|
+
totalDarkRevenue,
|
|
839
|
+
exactPurchaseLinkedPurchases,
|
|
840
|
+
exactPurchaseLinkedRevenue,
|
|
841
|
+
userLinkedPurchases,
|
|
842
|
+
userLinkedRevenue,
|
|
843
|
+
byAnswer: [
|
|
844
|
+
...byAnswer.values()
|
|
845
|
+
].sort((a, b) => b.revenue - a.revenue || b.purchases - a.purchases).slice(0, limit),
|
|
846
|
+
notes: [
|
|
847
|
+
"Report-only fallback. No purchase fields are mutated.",
|
|
848
|
+
"Exact purchase-linked responses use QuestionResponse.fields.purchaseId when present.",
|
|
849
|
+
"User-linked responses are lower-confidence and remain report-only."
|
|
850
|
+
]
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
__name(getCheckoutSurveyFallbackReport, "getCheckoutSurveyFallbackReport");
|
|
854
|
+
async function getValuePathSummary(range = "30d") {
|
|
855
|
+
if (!contactEvent || !sideEffectIntent) {
|
|
856
|
+
return {
|
|
857
|
+
contacts: 0,
|
|
858
|
+
events: 0,
|
|
859
|
+
intents: 0,
|
|
860
|
+
completedIntents: 0,
|
|
861
|
+
pendingIntents: 0,
|
|
862
|
+
blockedIntents: 0,
|
|
863
|
+
answerEvents: 0,
|
|
864
|
+
dripEvents: 0,
|
|
865
|
+
enteredEvents: 0,
|
|
866
|
+
participantsWithAnswerClicks: 0,
|
|
867
|
+
participantsWithNoAnswerClicks: 0,
|
|
868
|
+
terminalParticipants: 0,
|
|
869
|
+
answerOptions: [],
|
|
870
|
+
answerSteps: [],
|
|
871
|
+
terminalSteps: [],
|
|
872
|
+
notes: [
|
|
873
|
+
"Contact Event and SideEffectIntent tables are unavailable."
|
|
874
|
+
]
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const since = rangeToDate(range);
|
|
878
|
+
const eventConditions = [
|
|
879
|
+
sql`${contactEvent.eventType} LIKE 'value-path.%'`
|
|
880
|
+
];
|
|
881
|
+
const intentConditions = [
|
|
882
|
+
eq(sideEffectIntent.type, "send-value-path-email")
|
|
883
|
+
];
|
|
884
|
+
if (since) {
|
|
885
|
+
eventConditions.push(gte(contactEvent.occurredAt, since));
|
|
886
|
+
intentConditions.push(gte(sideEffectIntent.createdAt, since));
|
|
887
|
+
}
|
|
888
|
+
const [events, intents] = await Promise.all([
|
|
889
|
+
db.select({
|
|
890
|
+
contactId: contactEvent.contactId,
|
|
891
|
+
eventType: contactEvent.eventType,
|
|
892
|
+
providerEventId: contactEvent.providerEventId,
|
|
893
|
+
occurredAt: contactEvent.occurredAt
|
|
894
|
+
}).from(contactEvent).where(and(...eventConditions)),
|
|
895
|
+
db.select({
|
|
896
|
+
contactId: sideEffectIntent.contactId,
|
|
897
|
+
status: sideEffectIntent.status,
|
|
898
|
+
metadata: sideEffectIntent.metadata,
|
|
899
|
+
createdAt: sideEffectIntent.createdAt
|
|
900
|
+
}).from(sideEffectIntent).where(and(...intentConditions))
|
|
901
|
+
]);
|
|
902
|
+
const contactIds = /* @__PURE__ */ new Set();
|
|
903
|
+
for (const event of events)
|
|
904
|
+
contactIds.add(event.contactId);
|
|
905
|
+
for (const intent of intents)
|
|
906
|
+
contactIds.add(intent.contactId);
|
|
907
|
+
const answerEvents = events.filter((event) => event.eventType === "value-path.answer-selected");
|
|
908
|
+
const dripEvents = events.filter((event) => event.eventType === "value-path.drip-progressed");
|
|
909
|
+
const enteredEvents = events.filter((event) => event.eventType === "value-path.entered");
|
|
910
|
+
const answersByContact = /* @__PURE__ */ new Map();
|
|
911
|
+
for (const event of answerEvents) {
|
|
912
|
+
answersByContact.set(event.contactId, (answersByContact.get(event.contactId) ?? 0) + 1);
|
|
913
|
+
}
|
|
914
|
+
const answerKeyCounts = /* @__PURE__ */ new Map();
|
|
915
|
+
const answerStepCounts = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const event of answerEvents) {
|
|
917
|
+
const key = parseValuePathAnswerKey(event.providerEventId);
|
|
918
|
+
answerKeyCounts.set(key, (answerKeyCounts.get(key) ?? 0) + 1);
|
|
919
|
+
const step = answerStepFromKey(key);
|
|
920
|
+
answerStepCounts.set(step, (answerStepCounts.get(step) ?? 0) + 1);
|
|
921
|
+
}
|
|
922
|
+
const latestIntentByContact = /* @__PURE__ */ new Map();
|
|
923
|
+
for (const intent of [
|
|
924
|
+
...intents
|
|
925
|
+
].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())) {
|
|
926
|
+
latestIntentByContact.set(intent.contactId, intent);
|
|
927
|
+
}
|
|
928
|
+
const terminalCounts = /* @__PURE__ */ new Map();
|
|
929
|
+
for (const intent of latestIntentByContact.values()) {
|
|
930
|
+
const emailResourceId = String(intent.metadata?.emailResourceId ?? "");
|
|
931
|
+
if (!isTerminalValuePathEmail(emailResourceId))
|
|
932
|
+
continue;
|
|
933
|
+
terminalCounts.set(emailResourceId, (terminalCounts.get(emailResourceId) ?? 0) + 1);
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
contacts: contactIds.size,
|
|
937
|
+
events: events.length,
|
|
938
|
+
intents: intents.length,
|
|
939
|
+
completedIntents: intents.filter((intent) => intent.status === "completed").length,
|
|
940
|
+
pendingIntents: intents.filter((intent) => intent.status === "pending").length,
|
|
941
|
+
blockedIntents: intents.filter((intent) => intent.status === "blocked").length,
|
|
942
|
+
answerEvents: answerEvents.length,
|
|
943
|
+
dripEvents: dripEvents.length,
|
|
944
|
+
enteredEvents: enteredEvents.length,
|
|
945
|
+
participantsWithAnswerClicks: answersByContact.size,
|
|
946
|
+
participantsWithNoAnswerClicks: Math.max(0, contactIds.size - answersByContact.size),
|
|
947
|
+
terminalParticipants: [
|
|
948
|
+
...terminalCounts.values()
|
|
949
|
+
].reduce((total, count2) => total + count2, 0),
|
|
950
|
+
answerOptions: [
|
|
951
|
+
...answerKeyCounts.entries()
|
|
952
|
+
].map(([key, count2]) => ({
|
|
953
|
+
key,
|
|
954
|
+
step: answerStepFromKey(key),
|
|
955
|
+
optionValue: answerOptionFromKey(key),
|
|
956
|
+
count: count2
|
|
957
|
+
})).sort((a, b) => b.count - a.count),
|
|
958
|
+
answerSteps: [
|
|
959
|
+
...answerStepCounts.entries()
|
|
960
|
+
].map(([step, count2]) => ({
|
|
961
|
+
step,
|
|
962
|
+
count: count2
|
|
963
|
+
})).sort((a, b) => b.count - a.count),
|
|
964
|
+
terminalSteps: [
|
|
965
|
+
...terminalCounts.entries()
|
|
966
|
+
].map(([emailResourceId, count2]) => ({
|
|
967
|
+
emailResourceId,
|
|
968
|
+
count: count2
|
|
969
|
+
})).sort((a, b) => b.count - a.count),
|
|
970
|
+
notes: [
|
|
971
|
+
"Counts are based on Contact Events and SideEffectIntents.",
|
|
972
|
+
"Kit-native opens, unsubscribes, complaints, and raw Kit link clicks are not included."
|
|
973
|
+
]
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
__name(getValuePathSummary, "getValuePathSummary");
|
|
977
|
+
function parseValuePathAnswerKey(providerEventId) {
|
|
978
|
+
const match = String(providerEventId ?? "").match(/:answer:(.+)$/);
|
|
979
|
+
return match?.[1] ?? "unknown";
|
|
980
|
+
}
|
|
981
|
+
__name(parseValuePathAnswerKey, "parseValuePathAnswerKey");
|
|
982
|
+
function answerStepFromKey(key) {
|
|
983
|
+
const index = key.lastIndexOf(".");
|
|
984
|
+
return index > -1 ? key.slice(0, index) : key;
|
|
985
|
+
}
|
|
986
|
+
__name(answerStepFromKey, "answerStepFromKey");
|
|
987
|
+
function answerOptionFromKey(key) {
|
|
988
|
+
const index = key.lastIndexOf(".");
|
|
989
|
+
return index > -1 ? key.slice(index + 1) : "unknown";
|
|
990
|
+
}
|
|
991
|
+
__name(answerOptionFromKey, "answerOptionFromKey");
|
|
992
|
+
function isTerminalValuePathEmail(emailResourceId) {
|
|
993
|
+
return /(?:^|\.)(?:email-6|team-email-6)$/.test(emailResourceId);
|
|
994
|
+
}
|
|
995
|
+
__name(isTerminalValuePathEmail, "isTerminalValuePathEmail");
|
|
368
996
|
async function traceAttribution(opts) {
|
|
369
997
|
const events = [];
|
|
370
998
|
let userId = null;
|
|
@@ -521,8 +1149,12 @@ function createDatabaseProvider(db, schema) {
|
|
|
521
1149
|
getShortlinkPerformance,
|
|
522
1150
|
getRevenueBySource,
|
|
523
1151
|
getConversionFunnel,
|
|
1152
|
+
getCommerceLaneSummary,
|
|
524
1153
|
getAttributedRevenueSummary,
|
|
525
1154
|
getContentPurchaseCorrelation,
|
|
1155
|
+
getCheckoutAttributionReceipt,
|
|
1156
|
+
getCheckoutSurveyFallbackReport,
|
|
1157
|
+
getValuePathSummary,
|
|
526
1158
|
traceAttribution
|
|
527
1159
|
};
|
|
528
1160
|
}
|