@coursebuilder/analytics 1.1.1 → 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/types.d.ts CHANGED
@@ -8,6 +8,7 @@ type Percentage = Brand<number, 'Percentage'>;
8
8
  type Minutes = Brand<number, 'Minutes'>;
9
9
  type Seconds = Brand<number, 'Seconds'>;
10
10
  type AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all';
11
+ type TrafficAnalyticsRange = AnalyticsRange | '180d';
11
12
  interface RevenueSummary {
12
13
  totalRevenue: USD;
13
14
  purchaseCount: Count;
@@ -39,6 +40,8 @@ interface RecentPurchase {
39
40
  couponId: string | null;
40
41
  userName: string | null;
41
42
  userEmail: string | null;
43
+ isTeam?: boolean;
44
+ seats?: number | null;
42
45
  }
43
46
  interface AttributionCount {
44
47
  type: string;
@@ -49,6 +52,8 @@ interface ShortlinkPerformance {
49
52
  slug: string;
50
53
  url: string;
51
54
  clicks: Count;
55
+ signups?: Count;
56
+ purchases?: Count;
52
57
  }
53
58
  type CommerceRecordKind = 'paid_conversion' | 'access_grant' | 'synthetic' | 'unknown';
54
59
  interface RevenueBySource {
@@ -90,6 +95,20 @@ interface AttributionSignalSummary {
90
95
  recoveredFromShortlinkAttributionTable?: Count;
91
96
  internalFreeUpgrade: Count;
92
97
  }
98
+ type AttributionQualityLaneName = 'strong' | 'medium' | 'weak' | 'unknown';
99
+ interface AttributionQualityLane {
100
+ lane: AttributionQualityLaneName;
101
+ purchases: Count;
102
+ revenue: USD;
103
+ revenuePercent: Percentage;
104
+ notes: string[];
105
+ }
106
+ interface AttributionQualitySummary {
107
+ strong: AttributionQualityLane;
108
+ medium: AttributionQualityLane;
109
+ weak: AttributionQualityLane;
110
+ unknown: AttributionQualityLane;
111
+ }
93
112
  interface AttributionCoverage {
94
113
  totalRevenue: USD;
95
114
  attributedRevenue: USD;
@@ -107,6 +126,22 @@ interface AttributionCoverage {
107
126
  syntheticPurchases?: Count;
108
127
  commerce?: CommerceLaneSummary;
109
128
  signals?: AttributionSignalSummary;
129
+ quality?: AttributionQualitySummary;
130
+ }
131
+ interface TrafficBreakdownRow {
132
+ sessions: Count;
133
+ users: Count;
134
+ sessionPercent: Percentage;
135
+ trafficSessionPercent: Percentage;
136
+ }
137
+ interface DeviceCategoryBreakdown extends TrafficBreakdownRow {
138
+ deviceCategory: string;
139
+ }
140
+ interface OperatingSystemBreakdown extends TrafficBreakdownRow {
141
+ operatingSystem: string;
142
+ }
143
+ interface ScreenResolutionBreakdown extends TrafficBreakdownRow {
144
+ screenResolution: string;
110
145
  }
111
146
  interface TrafficOverview {
112
147
  sessions: Count;
@@ -115,6 +150,9 @@ interface TrafficOverview {
115
150
  pageviews: Count;
116
151
  avgSessionDuration: Seconds;
117
152
  bounceRate: Percentage;
153
+ deviceCategories?: DeviceCategoryBreakdown[];
154
+ operatingSystems?: OperatingSystemBreakdown[];
155
+ screenResolutions?: ScreenResolutionBreakdown[];
118
156
  }
119
157
  interface TrafficDaily {
120
158
  date: string;
@@ -448,4 +486,4 @@ type QueryResult<S extends SurfaceName> = {
448
486
  fix: string;
449
487
  };
450
488
 
451
- export type { AnalyticsRange, AttributionCount, AttributionCoverage, AttributionSignalSummary, CheckoutAttributionReceipt, CheckoutSurveyFallbackAnswer, CheckoutSurveyFallbackReport, CommerceLaneSummary, CommerceRecordKind, ContentCorrelation, ConversionFunnel, Count, EmailCampaignFunnel, EmailCampaignKitLink, EmailCampaignShortlink, EmailRevenueOverview, Minutes, Percentage, ProductSurveyRevenueAnswer, ProductSurveyRevenueCorrelation, QueryOptions, QueryResult, RecentPurchase, RevenueByCountry, RevenueByProduct, RevenueBySource, RevenueDaily, RevenueSummary, Seconds, ShortlinkPerformance, SurfaceMap, SurfaceName, SurveyConversionByQuestion, SurveyListItem, SurveyQuestionBreakdown, SurveyResponseRow, SurveyResponsesDaily, SurveyRevenueCorrelation, SurveySummary, TopPage, TrafficDaily, TrafficOverview, TrafficRevenueCorrelation, TrafficSource, USD, ValuePathAnswerBucket, ValuePathStepBucket, ValuePathSummary, ValuePathTerminalBucket, YouTubeChannelOverview, YouTubeDaily, YouTubeRevenueCorrelation, YouTubeTrafficSource, YouTubeVideoPerformance };
489
+ export type { AnalyticsRange, AttributionCount, AttributionCoverage, AttributionQualityLane, AttributionQualityLaneName, AttributionQualitySummary, AttributionSignalSummary, CheckoutAttributionReceipt, CheckoutSurveyFallbackAnswer, CheckoutSurveyFallbackReport, CommerceLaneSummary, CommerceRecordKind, ContentCorrelation, ConversionFunnel, Count, DeviceCategoryBreakdown, EmailCampaignFunnel, EmailCampaignKitLink, EmailCampaignShortlink, EmailRevenueOverview, Minutes, OperatingSystemBreakdown, Percentage, ProductSurveyRevenueAnswer, ProductSurveyRevenueCorrelation, QueryOptions, QueryResult, RecentPurchase, RevenueByCountry, RevenueByProduct, RevenueBySource, RevenueDaily, RevenueSummary, ScreenResolutionBreakdown, Seconds, ShortlinkPerformance, SurfaceMap, SurfaceName, SurveyConversionByQuestion, SurveyListItem, SurveyQuestionBreakdown, SurveyResponseRow, SurveyResponsesDaily, SurveyRevenueCorrelation, SurveySummary, TopPage, TrafficAnalyticsRange, TrafficBreakdownRow, TrafficDaily, TrafficOverview, TrafficRevenueCorrelation, TrafficSource, USD, ValuePathAnswerBucket, ValuePathStepBucket, ValuePathSummary, ValuePathTerminalBucket, YouTubeChannelOverview, YouTubeDaily, YouTubeRevenueCorrelation, YouTubeTrafficSource, YouTubeVideoPerformance };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coursebuilder/analytics",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-canary.0.0c9beb2ae",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@google-analytics/data": "^4.0.0",
73
- "zod": "^3.22.4"
73
+ "zod": "^4.1.12"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@types/react": "19.2.7",
@@ -83,10 +83,10 @@
83
83
  "tsup": "8.0.2",
84
84
  "typescript": "5.4.5",
85
85
  "vitest": "1.6.0",
86
- "@coursebuilder/ui": "2.0.12"
86
+ "@coursebuilder/ui": "2.4.1-canary.0.0c9beb2ae"
87
87
  },
88
88
  "peerDependencies": {
89
- "@coursebuilder/ui": "*",
89
+ "@coursebuilder/ui": "2.4.1-canary.0",
90
90
  "drizzle-orm": "*",
91
91
  "lucide-react": "*",
92
92
  "next": "*",
@@ -29,7 +29,7 @@ import {
29
29
  CardTitle,
30
30
  } from '@coursebuilder/ui'
31
31
 
32
- import type { AnalyticsRange } from '../types'
32
+ import type { AnalyticsRange, TrafficBreakdownRow } from '../types'
33
33
  import { CountryChart } from './country-chart'
34
34
  import { RevenueChart } from './revenue-chart'
35
35
 
@@ -107,6 +107,11 @@ export interface DashboardData {
107
107
  totalUsers: number
108
108
  newUsers: number
109
109
  pageviews: number
110
+ deviceCategories?: Array<TrafficBreakdownRow & { deviceCategory: string }>
111
+ operatingSystems?: Array<TrafficBreakdownRow & { operatingSystem: string }>
112
+ screenResolutions?: Array<
113
+ TrafficBreakdownRow & { screenResolution: string }
114
+ >
110
115
  } | null
111
116
  attributionCoverage: {
112
117
  totalRevenue: number
@@ -114,6 +119,16 @@ export interface DashboardData {
114
119
  unattributedRevenue: number
115
120
  attributionRate: number
116
121
  totalPurchases: number
122
+ quality?: Record<
123
+ 'strong' | 'medium' | 'weak' | 'unknown',
124
+ {
125
+ lane: 'strong' | 'medium' | 'weak' | 'unknown'
126
+ purchases: number
127
+ revenue: number
128
+ revenuePercent: number
129
+ notes: string[]
130
+ }
131
+ >
117
132
  } | null
118
133
  mux: VideoDashboardData | null
119
134
  muxThumbnails: Record<string, string>
@@ -618,6 +633,133 @@ function ShortlinksCard({
618
633
  )
619
634
  }
620
635
 
636
+ // ─── Traffic Breakdowns ──────────────────────────────────────────────────────
637
+
638
+ function fmtPct(v: number): string {
639
+ return `${v.toFixed(1)}%`
640
+ }
641
+
642
+ function TrafficBreakdownTable<T extends TrafficBreakdownRow>({
643
+ title,
644
+ label,
645
+ rows,
646
+ getName,
647
+ }: {
648
+ title: string
649
+ label: string
650
+ rows: T[]
651
+ getName: (row: T) => string
652
+ }) {
653
+ if (!rows.length) return null
654
+
655
+ return (
656
+ <section className="min-w-0">
657
+ <div className="mb-2 flex items-baseline justify-between gap-2">
658
+ <h3 className="text-sm font-semibold">{title}</h3>
659
+ <span className="text-muted-foreground text-[11px]">
660
+ Top {rows.length}
661
+ </span>
662
+ </div>
663
+ <div className="overflow-x-auto rounded-xl border border-border/50">
664
+ <table className="w-full min-w-[440px] text-left text-sm">
665
+ <thead className="bg-muted/30 text-muted-foreground text-[11px] uppercase tracking-wide">
666
+ <tr>
667
+ <th className="px-3 py-2 font-medium">{label}</th>
668
+ <th className="px-3 py-2 text-right font-medium">Sessions</th>
669
+ <th className="px-3 py-2 text-right font-medium">Users</th>
670
+ <th className="px-3 py-2 text-right font-medium">In rows</th>
671
+ <th className="px-3 py-2 text-right font-medium">Of traffic</th>
672
+ </tr>
673
+ </thead>
674
+ <tbody>
675
+ {rows.map((row) => (
676
+ <tr
677
+ key={getName(row)}
678
+ className="border-t border-border/40 tabular-nums"
679
+ >
680
+ <td className="max-w-[220px] truncate px-3 py-2 font-medium">
681
+ {getName(row)}
682
+ </td>
683
+ <td className="px-3 py-2 text-right">
684
+ {row.sessions.toLocaleString()}
685
+ </td>
686
+ <td className="px-3 py-2 text-right">
687
+ {row.users.toLocaleString()}
688
+ </td>
689
+ <td className="px-3 py-2 text-right">
690
+ {fmtPct(row.sessionPercent)}
691
+ </td>
692
+ <td className="px-3 py-2 text-right">
693
+ {fmtPct(row.trafficSessionPercent)}
694
+ </td>
695
+ </tr>
696
+ ))}
697
+ </tbody>
698
+ </table>
699
+ </div>
700
+ </section>
701
+ )
702
+ }
703
+
704
+ function TrafficBreakdownsCard({
705
+ traffic,
706
+ }: {
707
+ traffic: NonNullable<DashboardData['traffic']>
708
+ }) {
709
+ const deviceCategories = traffic.deviceCategories ?? []
710
+ const operatingSystems = traffic.operatingSystems ?? []
711
+ const screenResolutions = traffic.screenResolutions ?? []
712
+
713
+ if (
714
+ deviceCategories.length === 0 &&
715
+ operatingSystems.length === 0 &&
716
+ screenResolutions.length === 0
717
+ ) {
718
+ return null
719
+ }
720
+
721
+ return (
722
+ <Card>
723
+ <CardHeader>
724
+ <div className="flex items-center gap-2.5">
725
+ <GlobeIcon className="text-muted-foreground h-4 w-4" />
726
+ <div>
727
+ <CardTitle className="text-sm font-semibold">
728
+ Traffic Details
729
+ </CardTitle>
730
+ <CardDescription className="text-[11px]">
731
+ Device, OS, and screen share. In rows is share inside the table.
732
+ Of traffic is share of total sessions.
733
+ </CardDescription>
734
+ </div>
735
+ </div>
736
+ </CardHeader>
737
+ <CardContent>
738
+ <div className="grid gap-4 xl:grid-cols-3">
739
+ <TrafficBreakdownTable
740
+ title="Device category"
741
+ label="Device"
742
+ rows={deviceCategories}
743
+ getName={(row) => row.deviceCategory}
744
+ />
745
+ <TrafficBreakdownTable
746
+ title="Operating systems"
747
+ label="OS"
748
+ rows={operatingSystems.slice(0, 10)}
749
+ getName={(row) => row.operatingSystem}
750
+ />
751
+ <TrafficBreakdownTable
752
+ title="Screen resolutions"
753
+ label="Resolution"
754
+ rows={screenResolutions.slice(0, 10)}
755
+ getName={(row) => row.screenResolution}
756
+ />
757
+ </div>
758
+ </CardContent>
759
+ </Card>
760
+ )
761
+ }
762
+
621
763
  // ─── Range Selector ───────────────────────────────────────────────────────────
622
764
 
623
765
  const RANGES: { value: AnalyticsRange; label: string }[] = [
@@ -697,7 +839,7 @@ export function OmnibusDashboard({
697
839
  Analytics
698
840
  </h1>
699
841
  <p className="text-muted-foreground truncate text-[13px]">
700
- Revenue · Attribution · Video · Traffic · Surveys {rangeLabel}
842
+ Revenue · Attribution · Video · Traffic · Surveys, {rangeLabel}
701
843
  </p>
702
844
  </div>
703
845
  <div className="flex shrink-0 items-center gap-2">
@@ -796,6 +938,9 @@ export function OmnibusDashboard({
796
938
  )}
797
939
  </div>
798
940
 
941
+ {/* ── Traffic Details ──────────────────────────────────────── */}
942
+ {hasTraffic && <TrafficBreakdownsCard traffic={data.traffic!} />}
943
+
799
944
  {/* ── Video Performance ────────────────────────────────────── */}
800
945
  {hasMux && data.mux!.topVideos.length > 0 && (
801
946
  <TopVideosCard
@@ -854,7 +999,7 @@ export function OmnibusDashboard({
854
999
  <span className="text-foreground mt-1 block text-xl font-bold tabular-nums">
855
1000
  {signupCount > 0
856
1001
  ? `${((purchaseAttrCount / signupCount) * 100).toFixed(1)}%`
857
- : ''}
1002
+ : '-'}
858
1003
  </span>
859
1004
  </div>
860
1005
  </div>
@@ -912,6 +1057,32 @@ export function OmnibusDashboard({
912
1057
  </span>
913
1058
  </div>
914
1059
  </div>
1060
+ {data.attributionCoverage!.quality && (
1061
+ <div className="mt-4 grid gap-2 sm:grid-cols-4">
1062
+ {(['strong', 'medium', 'weak', 'unknown'] as const).map(
1063
+ (lane) => {
1064
+ const item = data.attributionCoverage!.quality![lane]
1065
+ return (
1066
+ <div
1067
+ key={lane}
1068
+ className="rounded-lg border border-border/40 p-3"
1069
+ >
1070
+ <span className="text-muted-foreground block text-[11px] font-medium uppercase tracking-wider">
1071
+ {lane}
1072
+ </span>
1073
+ <span className="text-foreground mt-1 block text-lg font-bold tabular-nums">
1074
+ {fmt$(item.revenue)}
1075
+ </span>
1076
+ <span className="text-muted-foreground text-[11px]">
1077
+ {item.purchases.toLocaleString()} purchases ·{' '}
1078
+ {(item.revenuePercent * 100).toFixed(1)}%
1079
+ </span>
1080
+ </div>
1081
+ )
1082
+ },
1083
+ )}
1084
+ </div>
1085
+ )}
915
1086
  </CardContent>
916
1087
  </Card>
917
1088
  )}
@@ -1593,18 +1764,18 @@ export function OmnibusDashboard({
1593
1764
  {p.totalAmount > 0 ? fmt$(p.totalAmount) : 'Free'}
1594
1765
  </td>
1595
1766
  <td className="py-2.5 pr-4 text-right text-sm tabular-nums">
1596
- {p.seats ?? ''}
1767
+ {p.seats ?? '-'}
1597
1768
  </td>
1598
1769
  <td className="max-w-[200px] truncate py-2.5 pr-4 text-sm font-medium">
1599
1770
  {p.productName}
1600
1771
  </td>
1601
1772
  <td className="text-muted-foreground hidden max-w-[150px] truncate py-2.5 pr-4 text-sm sm:table-cell">
1602
- {p.userName ?? p.userEmail ?? ''}
1773
+ {p.userName ?? p.userEmail ?? '-'}
1603
1774
  </td>
1604
1775
  <td className="text-muted-foreground hidden py-2.5 pr-4 text-sm sm:table-cell">
1605
1776
  {p.country
1606
1777
  ? `${countryFlag(p.country)} ${p.country}`
1607
- : ''}
1778
+ : '-'}
1608
1779
  </td>
1609
1780
  <td className="text-muted-foreground whitespace-nowrap py-2.5 pr-4 text-sm">
1610
1781
  {fmtAgo(p.createdAt)}
package/src/engine.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  QueryResult,
6
6
  SurfaceMap,
7
7
  SurfaceName,
8
+ TrafficAnalyticsRange,
8
9
  } from './types'
9
10
 
10
11
  export type AnalyticsProvider = Record<
@@ -14,7 +15,7 @@ export type AnalyticsProvider = Record<
14
15
 
15
16
  export type ProviderMap = Record<string, AnalyticsProvider>
16
17
 
17
- function toGA4Range(range: AnalyticsRange): '24h' | '7d' | '30d' | '90d' {
18
+ function toGA4Range(range: AnalyticsRange): TrafficAnalyticsRange {
18
19
  if (range === 'all') return '90d'
19
20
  return range
20
21
  }
@@ -71,7 +72,7 @@ async function invokeSurface<S extends SurfaceName>(
71
72
  case 'traffic/pages':
72
73
  case 'traffic/sources': {
73
74
  const fn = provider[entry.fn] as (
74
- range: '24h' | '7d' | '30d' | '90d',
75
+ range: TrafficAnalyticsRange,
75
76
  limit?: number,
76
77
  ) => Promise<SurfaceMap[S]>
77
78
  if (surface === 'traffic/pages' || surface === 'traffic/sources') {
@@ -143,6 +143,35 @@ export function createDatabaseProvider(
143
143
  OR ${exactShortlinkPurchaseAttributionSignalSql}
144
144
  )`
145
145
 
146
+ const strongAttributionQualitySql = sql`(
147
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.clickIds') IS NOT NULL
148
+ OR JSON_EXTRACT(${purchases.fields}, '$.attribution.utm') IS NOT NULL
149
+ OR JSON_EXTRACT(${purchases.fields}, '$.attribution.shortlink') IS NOT NULL
150
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.utmSource')) IS NOT NULL
151
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.shortlinkRef')) IS NOT NULL
152
+ )`
153
+
154
+ const mediumAttributionQualitySql = sql`(
155
+ NOT ${strongAttributionQualitySql}
156
+ AND ${exactShortlinkPurchaseAttributionSignalSql}
157
+ )`
158
+
159
+ const weakAttributionQualitySql = sql`(
160
+ NOT ${strongAttributionQualitySql}
161
+ AND NOT ${exactShortlinkPurchaseAttributionSignalSql}
162
+ AND (
163
+ JSON_EXTRACT(${purchases.fields}, '$.attribution.ga.clientId') IS NOT NULL
164
+ OR JSON_UNQUOTE(JSON_EXTRACT(${purchases.fields}, '$.gaClientId')) IS NOT NULL
165
+ OR JSON_EXTRACT(${purchases.fields}, '$.attribution.selfReportedSource') IS NOT NULL
166
+ )
167
+ )`
168
+
169
+ const unknownAttributionQualitySql = sql`(
170
+ NOT ${strongAttributionQualitySql}
171
+ AND NOT ${mediumAttributionQualitySql}
172
+ AND NOT ${weakAttributionQualitySql}
173
+ )`
174
+
146
175
  function rangeToDate(range: AnalyticsTimeRange): Date | null {
147
176
  if (range === 'all') return null
148
177
  const now = new Date()
@@ -690,6 +719,24 @@ export function createDatabaseProvider(
690
719
  .where(and(...signalConditions, condition))
691
720
  return row?.count ?? 0
692
721
  }
722
+ const qualityLane = async (
723
+ lane: 'strong' | 'medium' | 'weak' | 'unknown',
724
+ condition: any,
725
+ notes: string[],
726
+ ) => {
727
+ const [row] = await db
728
+ .select({ total: sum(purchases.totalAmount), count: count() })
729
+ .from(purchases)
730
+ .where(and(...conditions, condition))
731
+ const revenue = Number(row?.total ?? 0)
732
+ return {
733
+ lane,
734
+ purchases: row?.count ?? 0,
735
+ revenue,
736
+ revenuePercent: totalRevenue > 0 ? revenue / totalRevenue : 0,
737
+ notes,
738
+ }
739
+ }
693
740
  const [
694
741
  shortlinkCount,
695
742
  utmCount,
@@ -736,6 +783,21 @@ export function createDatabaseProvider(
736
783
  )
737
784
  const unattributedRevenue = totalRevenue - attributedRevenue
738
785
  const paidPurchases = totals?.count ?? 0
786
+ const [strongQuality, mediumQuality, weakQuality, unknownQuality] =
787
+ await Promise.all([
788
+ qualityLane('strong', strongAttributionQualitySql, [
789
+ 'Purchase-field UTM, shortlink, or paid click evidence.',
790
+ ]),
791
+ qualityLane('medium', mediumAttributionQualitySql, [
792
+ 'Recovered from exact shortlink attribution table purchase match.',
793
+ ]),
794
+ qualityLane('weak', weakAttributionQualitySql, [
795
+ 'GA client ID or self-reported source without stronger campaign evidence.',
796
+ ]),
797
+ qualityLane('unknown', unknownAttributionQualitySql, [
798
+ 'No usable attribution evidence for paid revenue.',
799
+ ]),
800
+ ])
739
801
 
740
802
  return {
741
803
  totalRevenue,
@@ -764,6 +826,12 @@ export function createDatabaseProvider(
764
826
  recoveredFromShortlinkAttributionTableCount,
765
827
  internalFreeUpgrade: commerce.freeUpgrades,
766
828
  },
829
+ quality: {
830
+ strong: strongQuality,
831
+ medium: mediumQuality,
832
+ weak: weakQuality,
833
+ unknown: unknownQuality,
834
+ },
767
835
  }
768
836
  }
769
837
 
package/src/types.ts CHANGED
@@ -9,6 +9,7 @@ export type Minutes = Brand<number, 'Minutes'>
9
9
  export type Seconds = Brand<number, 'Seconds'>
10
10
 
11
11
  export type AnalyticsRange = '24h' | '7d' | '30d' | '90d' | 'all'
12
+ export type TrafficAnalyticsRange = AnalyticsRange | '180d'
12
13
 
13
14
  export interface RevenueSummary {
14
15
  totalRevenue: USD
@@ -45,6 +46,8 @@ export interface RecentPurchase {
45
46
  couponId: string | null
46
47
  userName: string | null
47
48
  userEmail: string | null
49
+ isTeam?: boolean
50
+ seats?: number | null
48
51
  }
49
52
 
50
53
  export interface AttributionCount {
@@ -57,6 +60,8 @@ export interface ShortlinkPerformance {
57
60
  slug: string
58
61
  url: string
59
62
  clicks: Count
63
+ signups?: Count
64
+ purchases?: Count
60
65
  }
61
66
 
62
67
  export type CommerceRecordKind =
@@ -109,6 +114,27 @@ export interface AttributionSignalSummary {
109
114
  internalFreeUpgrade: Count
110
115
  }
111
116
 
117
+ export type AttributionQualityLaneName =
118
+ | 'strong'
119
+ | 'medium'
120
+ | 'weak'
121
+ | 'unknown'
122
+
123
+ export interface AttributionQualityLane {
124
+ lane: AttributionQualityLaneName
125
+ purchases: Count
126
+ revenue: USD
127
+ revenuePercent: Percentage
128
+ notes: string[]
129
+ }
130
+
131
+ export interface AttributionQualitySummary {
132
+ strong: AttributionQualityLane
133
+ medium: AttributionQualityLane
134
+ weak: AttributionQualityLane
135
+ unknown: AttributionQualityLane
136
+ }
137
+
112
138
  export interface AttributionCoverage {
113
139
  totalRevenue: USD
114
140
  attributedRevenue: USD
@@ -126,6 +152,26 @@ export interface AttributionCoverage {
126
152
  syntheticPurchases?: Count
127
153
  commerce?: CommerceLaneSummary
128
154
  signals?: AttributionSignalSummary
155
+ quality?: AttributionQualitySummary
156
+ }
157
+
158
+ export interface TrafficBreakdownRow {
159
+ sessions: Count
160
+ users: Count
161
+ sessionPercent: Percentage
162
+ trafficSessionPercent: Percentage
163
+ }
164
+
165
+ export interface DeviceCategoryBreakdown extends TrafficBreakdownRow {
166
+ deviceCategory: string
167
+ }
168
+
169
+ export interface OperatingSystemBreakdown extends TrafficBreakdownRow {
170
+ operatingSystem: string
171
+ }
172
+
173
+ export interface ScreenResolutionBreakdown extends TrafficBreakdownRow {
174
+ screenResolution: string
129
175
  }
130
176
 
131
177
  export interface TrafficOverview {
@@ -135,6 +181,9 @@ export interface TrafficOverview {
135
181
  pageviews: Count
136
182
  avgSessionDuration: Seconds
137
183
  bounceRate: Percentage
184
+ deviceCategories?: DeviceCategoryBreakdown[]
185
+ operatingSystems?: OperatingSystemBreakdown[]
186
+ screenResolutions?: ScreenResolutionBreakdown[]
138
187
  }
139
188
 
140
189
  export interface TrafficDaily {