@coursebuilder/analytics 1.1.1 → 1.1.2-canary.0.1de808999

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.
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { AnalyticsRange } from '../types.js';
2
+ import { TrafficBreakdownRow, AnalyticsRange } from '../types.js';
3
3
 
4
4
  /**
5
5
  * Data structure for the Mux video dashboard, inlined here so the shared
@@ -87,6 +87,15 @@ interface DashboardData {
87
87
  totalUsers: number;
88
88
  newUsers: number;
89
89
  pageviews: number;
90
+ deviceCategories?: Array<TrafficBreakdownRow & {
91
+ deviceCategory: string;
92
+ }>;
93
+ operatingSystems?: Array<TrafficBreakdownRow & {
94
+ operatingSystem: string;
95
+ }>;
96
+ screenResolutions?: Array<TrafficBreakdownRow & {
97
+ screenResolution: string;
98
+ }>;
90
99
  } | null;
91
100
  attributionCoverage: {
92
101
  totalRevenue: number;
@@ -94,6 +103,13 @@ interface DashboardData {
94
103
  unattributedRevenue: number;
95
104
  attributionRate: number;
96
105
  totalPurchases: number;
106
+ quality?: Record<'strong' | 'medium' | 'weak' | 'unknown', {
107
+ lane: 'strong' | 'medium' | 'weak' | 'unknown';
108
+ purchases: number;
109
+ revenue: number;
110
+ revenuePercent: number;
111
+ notes: string[];
112
+ }>;
97
113
  } | null;
98
114
  mux: VideoDashboardData | null;
99
115
  muxThumbnails: Record<string, string>;
@@ -684,6 +684,88 @@ function ShortlinksCard({ shortlinks, totalClicks }) {
684
684
  }), expanded ? "Show less" : `Show ${shortlinks.length - 10} more`)));
685
685
  }
686
686
  __name(ShortlinksCard, "ShortlinksCard");
687
+ function fmtPct(v) {
688
+ return `${v.toFixed(1)}%`;
689
+ }
690
+ __name(fmtPct, "fmtPct");
691
+ function TrafficBreakdownTable({ title, label, rows, getName }) {
692
+ if (!rows.length)
693
+ return null;
694
+ return /* @__PURE__ */ React3.createElement("section", {
695
+ className: "min-w-0"
696
+ }, /* @__PURE__ */ React3.createElement("div", {
697
+ className: "mb-2 flex items-baseline justify-between gap-2"
698
+ }, /* @__PURE__ */ React3.createElement("h3", {
699
+ className: "text-sm font-semibold"
700
+ }, title), /* @__PURE__ */ React3.createElement("span", {
701
+ className: "text-muted-foreground text-[11px]"
702
+ }, "Top ", rows.length)), /* @__PURE__ */ React3.createElement("div", {
703
+ className: "overflow-x-auto rounded-xl border border-border/50"
704
+ }, /* @__PURE__ */ React3.createElement("table", {
705
+ className: "w-full min-w-[440px] text-left text-sm"
706
+ }, /* @__PURE__ */ React3.createElement("thead", {
707
+ className: "bg-muted/30 text-muted-foreground text-[11px] uppercase tracking-wide"
708
+ }, /* @__PURE__ */ React3.createElement("tr", null, /* @__PURE__ */ React3.createElement("th", {
709
+ className: "px-3 py-2 font-medium"
710
+ }, label), /* @__PURE__ */ React3.createElement("th", {
711
+ className: "px-3 py-2 text-right font-medium"
712
+ }, "Sessions"), /* @__PURE__ */ React3.createElement("th", {
713
+ className: "px-3 py-2 text-right font-medium"
714
+ }, "Users"), /* @__PURE__ */ React3.createElement("th", {
715
+ className: "px-3 py-2 text-right font-medium"
716
+ }, "In rows"), /* @__PURE__ */ React3.createElement("th", {
717
+ className: "px-3 py-2 text-right font-medium"
718
+ }, "Of traffic"))), /* @__PURE__ */ React3.createElement("tbody", null, rows.map((row) => /* @__PURE__ */ React3.createElement("tr", {
719
+ key: getName(row),
720
+ className: "border-t border-border/40 tabular-nums"
721
+ }, /* @__PURE__ */ React3.createElement("td", {
722
+ className: "max-w-[220px] truncate px-3 py-2 font-medium"
723
+ }, getName(row)), /* @__PURE__ */ React3.createElement("td", {
724
+ className: "px-3 py-2 text-right"
725
+ }, row.sessions.toLocaleString()), /* @__PURE__ */ React3.createElement("td", {
726
+ className: "px-3 py-2 text-right"
727
+ }, row.users.toLocaleString()), /* @__PURE__ */ React3.createElement("td", {
728
+ className: "px-3 py-2 text-right"
729
+ }, fmtPct(row.sessionPercent)), /* @__PURE__ */ React3.createElement("td", {
730
+ className: "px-3 py-2 text-right"
731
+ }, fmtPct(row.trafficSessionPercent))))))));
732
+ }
733
+ __name(TrafficBreakdownTable, "TrafficBreakdownTable");
734
+ function TrafficBreakdownsCard({ traffic }) {
735
+ const deviceCategories = traffic.deviceCategories ?? [];
736
+ const operatingSystems = traffic.operatingSystems ?? [];
737
+ const screenResolutions = traffic.screenResolutions ?? [];
738
+ if (deviceCategories.length === 0 && operatingSystems.length === 0 && screenResolutions.length === 0) {
739
+ return null;
740
+ }
741
+ return /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, null, /* @__PURE__ */ React3.createElement("div", {
742
+ className: "flex items-center gap-2.5"
743
+ }, /* @__PURE__ */ React3.createElement(GlobeIcon, {
744
+ className: "text-muted-foreground h-4 w-4"
745
+ }), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
746
+ className: "text-sm font-semibold"
747
+ }, "Traffic Details"), /* @__PURE__ */ React3.createElement(CardDescription, {
748
+ className: "text-[11px]"
749
+ }, "Device, OS, and screen share. In rows is share inside the table. Of traffic is share of total sessions.")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
750
+ className: "grid gap-4 xl:grid-cols-3"
751
+ }, /* @__PURE__ */ React3.createElement(TrafficBreakdownTable, {
752
+ title: "Device category",
753
+ label: "Device",
754
+ rows: deviceCategories,
755
+ getName: (row) => row.deviceCategory
756
+ }), /* @__PURE__ */ React3.createElement(TrafficBreakdownTable, {
757
+ title: "Operating systems",
758
+ label: "OS",
759
+ rows: operatingSystems.slice(0, 10),
760
+ getName: (row) => row.operatingSystem
761
+ }), /* @__PURE__ */ React3.createElement(TrafficBreakdownTable, {
762
+ title: "Screen resolutions",
763
+ label: "Resolution",
764
+ rows: screenResolutions.slice(0, 10),
765
+ getName: (row) => row.screenResolution
766
+ }))));
767
+ }
768
+ __name(TrafficBreakdownsCard, "TrafficBreakdownsCard");
687
769
  var RANGES = [
688
770
  {
689
771
  value: "24h",
@@ -743,7 +825,7 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
743
825
  className: "text-pretty text-xl font-bold tracking-tight sm:text-2xl"
744
826
  }, "Analytics"), /* @__PURE__ */ React3.createElement("p", {
745
827
  className: "text-muted-foreground truncate text-[13px]"
746
- }, "Revenue \xB7 Attribution \xB7 Video \xB7 Traffic \xB7 Surveys \u2014 ", rangeLabel)), /* @__PURE__ */ React3.createElement("div", {
828
+ }, "Revenue \xB7 Attribution \xB7 Video \xB7 Traffic \xB7 Surveys, ", rangeLabel)), /* @__PURE__ */ React3.createElement("div", {
747
829
  className: "flex shrink-0 items-center gap-2"
748
830
  }, isPending && /* @__PURE__ */ React3.createElement("span", {
749
831
  className: "text-muted-foreground animate-pulse text-xs"
@@ -798,7 +880,9 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
798
880
  value: `${data.surveySegments.length}`,
799
881
  sub: `${data.surveySegments.reduce((s, q) => s + q.responses, 0).toLocaleString()} responses`,
800
882
  icon: ClipboardListIcon
801
- })), hasMux && data.mux.topVideos.length > 0 && /* @__PURE__ */ React3.createElement(TopVideosCard, {
883
+ })), hasTraffic && /* @__PURE__ */ React3.createElement(TrafficBreakdownsCard, {
884
+ traffic: data.traffic
885
+ }), hasMux && data.mux.topVideos.length > 0 && /* @__PURE__ */ React3.createElement(TopVideosCard, {
802
886
  label: "Site Videos",
803
887
  subtitle: `${fmtWatchMs(data.mux.overview.totalPlayingTimeMs)} watch time \xB7 ${fmtK(data.mux.overview.uniqueViewers)} viewers`,
804
888
  icon: FilmIcon,
@@ -840,7 +924,7 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
840
924
  className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
841
925
  }, "Conversion Rate"), /* @__PURE__ */ React3.createElement("span", {
842
926
  className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
843
- }, signupCount > 0 ? `${(purchaseAttrCount / signupCount * 100).toFixed(1)}%` : "\u2014"))))), hasAttributionCoverage && data.attributionCoverage.totalPurchases > 0 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
927
+ }, signupCount > 0 ? `${(purchaseAttrCount / signupCount * 100).toFixed(1)}%` : "-"))))), hasAttributionCoverage && data.attributionCoverage.totalPurchases > 0 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
844
928
  className: "pb-3"
845
929
  }, /* @__PURE__ */ React3.createElement("div", {
846
930
  className: "flex items-center gap-2.5"
@@ -872,7 +956,26 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
872
956
  className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
873
957
  }, "Total Purchases"), /* @__PURE__ */ React3.createElement("span", {
874
958
  className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
875
- }, data.attributionCoverage.totalPurchases.toLocaleString()))))), hasValuePaths && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
959
+ }, data.attributionCoverage.totalPurchases.toLocaleString()))), data.attributionCoverage.quality && /* @__PURE__ */ React3.createElement("div", {
960
+ className: "mt-4 grid gap-2 sm:grid-cols-4"
961
+ }, [
962
+ "strong",
963
+ "medium",
964
+ "weak",
965
+ "unknown"
966
+ ].map((lane) => {
967
+ const item = data.attributionCoverage.quality[lane];
968
+ return /* @__PURE__ */ React3.createElement("div", {
969
+ key: lane,
970
+ className: "rounded-lg border border-border/40 p-3"
971
+ }, /* @__PURE__ */ React3.createElement("span", {
972
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
973
+ }, lane), /* @__PURE__ */ React3.createElement("span", {
974
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
975
+ }, fmt$(item.revenue)), /* @__PURE__ */ React3.createElement("span", {
976
+ className: "text-muted-foreground text-[11px]"
977
+ }, item.purchases.toLocaleString(), " purchases \xB7", " ", (item.revenuePercent * 100).toFixed(1), "%"));
978
+ })))), hasValuePaths && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
876
979
  className: "pb-3"
877
980
  }, /* @__PURE__ */ React3.createElement("div", {
878
981
  className: "flex items-center gap-2.5"
@@ -1327,13 +1430,13 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
1327
1430
  className: `py-2.5 pr-4 text-right text-sm font-semibold tabular-nums ${p.totalAmount > 0 ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"}`
1328
1431
  }, p.totalAmount > 0 ? fmt$(p.totalAmount) : "Free"), /* @__PURE__ */ React3.createElement("td", {
1329
1432
  className: "py-2.5 pr-4 text-right text-sm tabular-nums"
1330
- }, p.seats ?? "\u2014"), /* @__PURE__ */ React3.createElement("td", {
1433
+ }, p.seats ?? "-"), /* @__PURE__ */ React3.createElement("td", {
1331
1434
  className: "max-w-[200px] truncate py-2.5 pr-4 text-sm font-medium"
1332
1435
  }, p.productName), /* @__PURE__ */ React3.createElement("td", {
1333
1436
  className: "text-muted-foreground hidden max-w-[150px] truncate py-2.5 pr-4 text-sm sm:table-cell"
1334
- }, p.userName ?? p.userEmail ?? "\u2014"), /* @__PURE__ */ React3.createElement("td", {
1437
+ }, p.userName ?? p.userEmail ?? "-"), /* @__PURE__ */ React3.createElement("td", {
1335
1438
  className: "text-muted-foreground hidden py-2.5 pr-4 text-sm sm:table-cell"
1336
- }, p.country ? `${countryFlag2(p.country)} ${p.country}` : "\u2014"), /* @__PURE__ */ React3.createElement("td", {
1439
+ }, p.country ? `${countryFlag2(p.country)} ${p.country}` : "-"), /* @__PURE__ */ React3.createElement("td", {
1337
1440
  className: "text-muted-foreground whitespace-nowrap py-2.5 pr-4 text-sm"
1338
1441
  }, fmtAgo(p.createdAt)))))))))));
1339
1442
  }