lytx 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { DasboardDataResult, Pagination } from "@db/types";
|
|
2
|
+
|
|
3
|
+
export interface NivoBarChartData {
|
|
4
|
+
data: Record<string, any>[];
|
|
5
|
+
keys: string[];
|
|
6
|
+
indexBy: string;
|
|
7
|
+
axisBottom?: any;
|
|
8
|
+
axisLeft?: any;
|
|
9
|
+
legends?: any[];
|
|
10
|
+
options: { chart: { type: "bar" } }; // Mandatory type
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NivoPieChartData {
|
|
14
|
+
data: { id: string | number; value: number }[];
|
|
15
|
+
legends?: any[];
|
|
16
|
+
options: { chart: { type: "pie" } }; // Mandatory type
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NivoLineChartData {
|
|
20
|
+
data: {
|
|
21
|
+
id: string | number;
|
|
22
|
+
data: { x: string | number; y: string | number }[];
|
|
23
|
+
}[];
|
|
24
|
+
legends?: any[]; // Optional
|
|
25
|
+
options: { chart: { type: "line" } }; // Mandatory type
|
|
26
|
+
axisBottom?: any; // Optional, can be customized per chart
|
|
27
|
+
axisLeft?: any; // Optional
|
|
28
|
+
// Add other line-specific Nivo props if they need to be dynamic from data source
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type NivoChartData =
|
|
32
|
+
| NivoBarChartData
|
|
33
|
+
| NivoPieChartData
|
|
34
|
+
| NivoLineChartData;
|
|
35
|
+
|
|
36
|
+
export type ScoreCardLabels =
|
|
37
|
+
| "Uniques"
|
|
38
|
+
| "Total Page Views"
|
|
39
|
+
| "Bounce Rate"
|
|
40
|
+
| "Conversion Rate"
|
|
41
|
+
| "Revenue"
|
|
42
|
+
| "Avg Session Duration"
|
|
43
|
+
| "avg_time_on_page"
|
|
44
|
+
| "pages_per_session"
|
|
45
|
+
| "new_users";
|
|
46
|
+
export interface ScorecardProps {
|
|
47
|
+
title: ScoreCardLabels;
|
|
48
|
+
value: string;
|
|
49
|
+
change: string;
|
|
50
|
+
changeType: "positive" | "negative" | "neutral";
|
|
51
|
+
changeLabel: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ChartComponentProps {
|
|
55
|
+
chartId: string;
|
|
56
|
+
chartData: NivoChartData | null | undefined;
|
|
57
|
+
title: string;
|
|
58
|
+
isLoading: boolean;
|
|
59
|
+
type: "bar" | "pie" | "line";
|
|
60
|
+
height?: string | number;
|
|
61
|
+
onItemClick?: (id: string) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Props for TableComponent
|
|
65
|
+
export interface TableComponentProps {
|
|
66
|
+
tableId: string;
|
|
67
|
+
tableData:
|
|
68
|
+
| {
|
|
69
|
+
headers: string[];
|
|
70
|
+
rows: (string | number)[][];
|
|
71
|
+
title?: string;
|
|
72
|
+
}
|
|
73
|
+
| null
|
|
74
|
+
| undefined;
|
|
75
|
+
title?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Helper function to serialize data for client components
|
|
79
|
+
function serializeForClient<T>(obj: T): T {
|
|
80
|
+
return JSON.parse(
|
|
81
|
+
JSON.stringify(obj, (_key, value) => {
|
|
82
|
+
if (value instanceof Date) {
|
|
83
|
+
return value.toISOString();
|
|
84
|
+
}
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function scoreCardCounts(data: DasboardDataResult): Array<ScorecardProps> {
|
|
93
|
+
// Scorecards are derived from raw event rows for a site/date range.
|
|
94
|
+
// `rid` is treated as a "session id"; we use distinct `rid` values as "unique visitors".
|
|
95
|
+
let uniqueCount = 0;
|
|
96
|
+
|
|
97
|
+
// Total page views = number of `page_view` events.
|
|
98
|
+
const totalPageViewsCount = data.filter((d) => d.event == "page_view").length;
|
|
99
|
+
|
|
100
|
+
// Unique visitors = count of distinct `rid` values.
|
|
101
|
+
if (data.length > 0) {
|
|
102
|
+
const uniqueMap = new Map<string, number>();
|
|
103
|
+
for (const item of data) {
|
|
104
|
+
if (!item.rid) continue;
|
|
105
|
+
const check = uniqueMap.get(item.rid);
|
|
106
|
+
uniqueMap.set(item.rid, check ? check + 1 : 1);
|
|
107
|
+
}
|
|
108
|
+
const ridsArray = Array.from(uniqueMap);
|
|
109
|
+
uniqueCount = ridsArray.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const uniques: ScorecardProps = {
|
|
113
|
+
title: "Uniques",
|
|
114
|
+
value: `${uniqueCount.toLocaleString()}`,
|
|
115
|
+
change: "",
|
|
116
|
+
changeType: "neutral",
|
|
117
|
+
changeLabel: "",
|
|
118
|
+
};
|
|
119
|
+
const totalPageViews: ScorecardProps = {
|
|
120
|
+
title: "Total Page Views",
|
|
121
|
+
value: `${totalPageViewsCount.toLocaleString()}`,
|
|
122
|
+
change: "",
|
|
123
|
+
changeType: "neutral",
|
|
124
|
+
changeLabel: "",
|
|
125
|
+
};
|
|
126
|
+
// Bounce rate = % of sessions (`rid`) with exactly one page view.
|
|
127
|
+
const sessionPageViews = new Map<string, number>();
|
|
128
|
+
data
|
|
129
|
+
.filter((d) => d.event === "page_view" && d.rid)
|
|
130
|
+
.forEach((d) => {
|
|
131
|
+
const current = sessionPageViews.get(d.rid!) || 0;
|
|
132
|
+
sessionPageViews.set(d.rid!, current + 1);
|
|
133
|
+
});
|
|
134
|
+
const singlePageSessions = Array.from(sessionPageViews.values()).filter(
|
|
135
|
+
(count) => count === 1,
|
|
136
|
+
).length;
|
|
137
|
+
const bounceRatePercent =
|
|
138
|
+
uniqueCount > 0
|
|
139
|
+
? ((singlePageSessions / uniqueCount) * 100).toFixed(1)
|
|
140
|
+
: "0";
|
|
141
|
+
|
|
142
|
+
// Conversion rate = % of sessions (`rid`) that include a conversion-like event.
|
|
143
|
+
const conversionEvents = data.filter(
|
|
144
|
+
(d) => d.event === "conversion" || d.event === "purchase",
|
|
145
|
+
).length;
|
|
146
|
+
const conversionRatePercent =
|
|
147
|
+
uniqueCount > 0
|
|
148
|
+
? ((conversionEvents / uniqueCount) * 100).toFixed(2)
|
|
149
|
+
: "0.00";
|
|
150
|
+
|
|
151
|
+
// Calculate total revenue - placeholder since no value field exists
|
|
152
|
+
const totalRevenue = 0; // Would need a value field in the data structure
|
|
153
|
+
|
|
154
|
+
// Average session duration (simplified): per `rid`, take (max(createdAt)-min(createdAt)).
|
|
155
|
+
const sessionTimes = new Map<string, { first: Date; last: Date }>();
|
|
156
|
+
data
|
|
157
|
+
.filter((d) => d.rid && d.createdAt)
|
|
158
|
+
.forEach((d) => {
|
|
159
|
+
const timestamp = d.createdAt!;
|
|
160
|
+
const existing = sessionTimes.get(d.rid!);
|
|
161
|
+
if (!existing) {
|
|
162
|
+
sessionTimes.set(d.rid!, { first: timestamp, last: timestamp });
|
|
163
|
+
} else {
|
|
164
|
+
if (timestamp < existing.first) existing.first = timestamp;
|
|
165
|
+
if (timestamp > existing.last) existing.last = timestamp;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const sessionDurations = Array.from(sessionTimes.values()).map(
|
|
170
|
+
({ first, last }) => (last.getTime() - first.getTime()) / 1000,
|
|
171
|
+
); // in seconds
|
|
172
|
+
const avgDuration =
|
|
173
|
+
sessionDurations.length > 0
|
|
174
|
+
? sessionDurations.reduce((sum, duration) => sum + duration, 0) /
|
|
175
|
+
sessionDurations.length
|
|
176
|
+
: 0;
|
|
177
|
+
const avgDurationFormatted =
|
|
178
|
+
avgDuration > 60
|
|
179
|
+
? `${Math.floor(avgDuration / 60)}m ${Math.floor(avgDuration % 60)}s`
|
|
180
|
+
: `${Math.floor(avgDuration)}s`;
|
|
181
|
+
|
|
182
|
+
const bounceRate: ScorecardProps = {
|
|
183
|
+
title: "Bounce Rate",
|
|
184
|
+
value: `${bounceRatePercent}%`,
|
|
185
|
+
change: "",
|
|
186
|
+
changeType: "neutral",
|
|
187
|
+
changeLabel: "",
|
|
188
|
+
};
|
|
189
|
+
const conversionRate: ScorecardProps = {
|
|
190
|
+
title: "Conversion Rate",
|
|
191
|
+
value: `${conversionRatePercent}%`,
|
|
192
|
+
change: "",
|
|
193
|
+
changeType: "neutral",
|
|
194
|
+
changeLabel: "",
|
|
195
|
+
};
|
|
196
|
+
const revenue: ScorecardProps = {
|
|
197
|
+
title: "Revenue",
|
|
198
|
+
value: `$${totalRevenue.toLocaleString()}`,
|
|
199
|
+
change: "",
|
|
200
|
+
changeType: "neutral",
|
|
201
|
+
changeLabel: "",
|
|
202
|
+
};
|
|
203
|
+
const avgSessionDuration: ScorecardProps = {
|
|
204
|
+
title: "Avg Session Duration",
|
|
205
|
+
value: avgDurationFormatted,
|
|
206
|
+
change: "",
|
|
207
|
+
changeType: "neutral",
|
|
208
|
+
changeLabel: "",
|
|
209
|
+
};
|
|
210
|
+
// const pagesPerSession: ScorecardProps = {
|
|
211
|
+
// title: "Pages per Session",
|
|
212
|
+
// value: "12,345",
|
|
213
|
+
// change: "",
|
|
214
|
+
// changeType: "neutral",
|
|
215
|
+
// changeLabel: "",
|
|
216
|
+
// };
|
|
217
|
+
// const newUsers: ScorecardProps = {
|
|
218
|
+
// title: "New Users",
|
|
219
|
+
// value: "12,345",
|
|
220
|
+
// change: "",
|
|
221
|
+
// changeType: "neutral",
|
|
222
|
+
// changeLabel: "",
|
|
223
|
+
// };
|
|
224
|
+
|
|
225
|
+
return [
|
|
226
|
+
uniques,
|
|
227
|
+
totalPageViews,
|
|
228
|
+
bounceRate,
|
|
229
|
+
conversionRate,
|
|
230
|
+
revenue,
|
|
231
|
+
avgSessionDuration,
|
|
232
|
+
// avgTimeOnPage,
|
|
233
|
+
// pagesPerSession,
|
|
234
|
+
// newUsers,
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function transformToChartData(data: DasboardDataResult) {
|
|
239
|
+
// Group the data by date
|
|
240
|
+
const dateCountMap = new Map<string, number>();
|
|
241
|
+
const eventCountMap = new Map<string, number>();
|
|
242
|
+
const refererCountMap = new Map<string, number>();
|
|
243
|
+
const debvCountMap = new Map<string, number>();
|
|
244
|
+
const topPagesMap = new Map<string, number>();
|
|
245
|
+
const browserMap = new Map<string, number>();
|
|
246
|
+
const osMap = new Map<string, number>();
|
|
247
|
+
|
|
248
|
+
const cityMap = new Map<string, { count: number; country: string }>();
|
|
249
|
+
|
|
250
|
+
// Array<{ id: string, value: number }
|
|
251
|
+
|
|
252
|
+
data.forEach((item) => {
|
|
253
|
+
const currentCount = eventCountMap.get(item.event!) || 0;
|
|
254
|
+
eventCountMap.set(item.event!, currentCount + 1);
|
|
255
|
+
const currentCity = cityMap.get(item.city!);
|
|
256
|
+
if (currentCity) {
|
|
257
|
+
currentCity.count++;
|
|
258
|
+
} else {
|
|
259
|
+
cityMap.set(item.city!, { count: 1, country: item.country! });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const currentReferer =
|
|
263
|
+
refererCountMap.get(cleanReferer(item.referer!)) || 0;
|
|
264
|
+
refererCountMap.set(cleanReferer(item.referer!), currentReferer + 1);
|
|
265
|
+
|
|
266
|
+
const deviceType = item.device_type || "Unknown";
|
|
267
|
+
const currentDebv = debvCountMap.get(deviceType) || 0;
|
|
268
|
+
debvCountMap.set(deviceType, currentDebv + 1);
|
|
269
|
+
|
|
270
|
+
const currentBrowser = browserMap.get(item.browser!) || 0;
|
|
271
|
+
browserMap.set(item.browser!, currentBrowser + 1);
|
|
272
|
+
|
|
273
|
+
// Aggregate operating system data
|
|
274
|
+
if (item.operating_system) {
|
|
275
|
+
const currentOs = osMap.get(item.operating_system) || 0;
|
|
276
|
+
osMap.set(item.operating_system, currentOs + 1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const currentTopPage = topPagesMap.get(item.client_page_url!) || 0;
|
|
280
|
+
topPagesMap.set(item.client_page_url!, currentTopPage + 1);
|
|
281
|
+
|
|
282
|
+
if (item.event === "page_view") {
|
|
283
|
+
// Format the date (extract just the YYYY-MM-DD part)
|
|
284
|
+
const dateStr = item.createdAt!.toISOString().split("T")[0];
|
|
285
|
+
|
|
286
|
+
// Increment the count for this date
|
|
287
|
+
const currentCount = dateCountMap.get(dateStr) || 0;
|
|
288
|
+
dateCountMap.set(dateStr, currentCount + 1);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Convert the map to the required output format
|
|
293
|
+
const result = Array.from(dateCountMap.entries()).map(([date, count]) => ({
|
|
294
|
+
x: date,
|
|
295
|
+
y: count,
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
// Sort by date
|
|
299
|
+
result.sort((a, b) => a.x.localeCompare(b.x));
|
|
300
|
+
|
|
301
|
+
const transformedData = {
|
|
302
|
+
pageViews: result,
|
|
303
|
+
scoreCards: scoreCardCounts(data),
|
|
304
|
+
events: Array.from(eventCountMap.entries()),
|
|
305
|
+
devices: Array.from(debvCountMap.entries()),
|
|
306
|
+
browsers: Array.from(browserMap.entries())
|
|
307
|
+
.toSorted((a, b) => b[1] - a[1])
|
|
308
|
+
.map((a) => {
|
|
309
|
+
return { id: a[0], value: a[1] };
|
|
310
|
+
}),
|
|
311
|
+
operatingSystems: Array.from(osMap.entries())
|
|
312
|
+
.toSorted((a, b) => b[1] - a[1])
|
|
313
|
+
.map((a) => {
|
|
314
|
+
return { id: a[0], value: a[1] };
|
|
315
|
+
}),
|
|
316
|
+
cities: Array.from(cityMap.entries()).toSorted(
|
|
317
|
+
(a, b) => b[1].count - a[1].count,
|
|
318
|
+
),
|
|
319
|
+
topPages: Array.from(topPagesMap.entries())
|
|
320
|
+
.toSorted((a, b) => a[1] - b[1])
|
|
321
|
+
.map((a) => {
|
|
322
|
+
return { id: a[0], value: a[1] };
|
|
323
|
+
}),
|
|
324
|
+
referers: Array.from(refererCountMap.entries())
|
|
325
|
+
.toSorted((a, b) => b[1] - a[1])
|
|
326
|
+
.map((a) => {
|
|
327
|
+
return { id: a[0], value: a[1] };
|
|
328
|
+
}),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Ensure all data is serializable for client components
|
|
332
|
+
return serializeForClient(transformedData);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function getPageViewsData(
|
|
336
|
+
data?: Array<{ x: string; y: number }>,
|
|
337
|
+
): NivoLineChartData {
|
|
338
|
+
const points = (data || []).map((item) => ({
|
|
339
|
+
x: item.x,
|
|
340
|
+
y: item.y,
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
options: { chart: { type: "line" as const } },
|
|
345
|
+
data: [
|
|
346
|
+
{
|
|
347
|
+
id: "Page Views",
|
|
348
|
+
data: points,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// function getDateTickValues(values: string[]): string[] | undefined {
|
|
355
|
+
// const total = values.length;
|
|
356
|
+
// if (total <= 10) return values;
|
|
357
|
+
//
|
|
358
|
+
// const targetTicks = total <= 30 ? 8 : total <= 90 ? 10 : 12;
|
|
359
|
+
// const step = Math.max(1, Math.ceil(total / targetTicks));
|
|
360
|
+
// const ticks = values.filter((_, index) => index % step === 0);
|
|
361
|
+
// const last = values[values.length - 1];
|
|
362
|
+
//
|
|
363
|
+
// if (ticks[ticks.length - 1] !== last) {
|
|
364
|
+
// ticks.push(last);
|
|
365
|
+
// }
|
|
366
|
+
//
|
|
367
|
+
// return ticks;
|
|
368
|
+
// }
|
|
369
|
+
|
|
370
|
+
// function formatDateTick(value: string | number): string {
|
|
371
|
+
// const raw = String(value);
|
|
372
|
+
//
|
|
373
|
+
// if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
374
|
+
// const parts = raw.split("-");
|
|
375
|
+
// return `${parts[1]}/${parts[2]}`;
|
|
376
|
+
// }
|
|
377
|
+
//
|
|
378
|
+
// if (/^\d{4}-\d{2}$/.test(raw)) {
|
|
379
|
+
// const parts = raw.split("-");
|
|
380
|
+
// return `${parts[1]}/${parts[0]}`;
|
|
381
|
+
// }
|
|
382
|
+
//
|
|
383
|
+
// return raw;
|
|
384
|
+
// }
|
|
385
|
+
|
|
386
|
+
export function getEventTypesData(
|
|
387
|
+
data?: Array<[string, number]>,
|
|
388
|
+
): TableComponentProps["tableData"] {
|
|
389
|
+
const defaultData = [
|
|
390
|
+
["page_view", 2500],
|
|
391
|
+
["add_to_cart", 300],
|
|
392
|
+
["checkout_start", 150],
|
|
393
|
+
["video_play", 500],
|
|
394
|
+
["download_pdf", 120],
|
|
395
|
+
];
|
|
396
|
+
return {
|
|
397
|
+
headers: ["Event Name", "Count"],
|
|
398
|
+
rows: data || defaultData,
|
|
399
|
+
title: "Event Types",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function getDeviceGeoData(data?: {
|
|
404
|
+
deviceData?: Array<[string, number]>;
|
|
405
|
+
geoData?: Array<[string, string, number]>;
|
|
406
|
+
}) {
|
|
407
|
+
let defaultDeviceData = [
|
|
408
|
+
{ id: "Desktop", value: 65 },
|
|
409
|
+
{ id: "Mobile", value: 25 },
|
|
410
|
+
{ id: "Tablet", value: 10 },
|
|
411
|
+
];
|
|
412
|
+
if (data) {
|
|
413
|
+
if (data.deviceData) {
|
|
414
|
+
defaultDeviceData = data.deviceData.map(([id, value]) => ({ id, value }));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const defaultGeoData = [["Canada", "Toronto", 400]];
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
deviceTypes: {
|
|
422
|
+
options: { chart: { type: "pie" as const } },
|
|
423
|
+
data: defaultDeviceData,
|
|
424
|
+
},
|
|
425
|
+
geoData: {
|
|
426
|
+
headers: ["Country", "City", "Views"],
|
|
427
|
+
rows: data?.geoData || defaultGeoData,
|
|
428
|
+
title: "Top Geo Locations",
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function getTopSourcesData(
|
|
434
|
+
data?: Array<{ name: string; visitors: number }>,
|
|
435
|
+
) {
|
|
436
|
+
const defaultData = [
|
|
437
|
+
{ name: "Google", visitors: 5200, icon: "google.svg" },
|
|
438
|
+
{ name: "Direct", visitors: 2100, icon: "direct.svg" },
|
|
439
|
+
{ name: "Facebook", visitors: 1500, icon: "facebook.svg" },
|
|
440
|
+
{ name: "Twitter", visitors: 900, icon: "twitter.svg" },
|
|
441
|
+
];
|
|
442
|
+
return data || defaultData;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function getTopPagesData(
|
|
446
|
+
data?: Array<{ id: string; value: number }>,
|
|
447
|
+
): NivoBarChartData {
|
|
448
|
+
const defaultData = [
|
|
449
|
+
{ id: "/home", value: 3050 },
|
|
450
|
+
{ id: "/products", value: 2200 },
|
|
451
|
+
{ id: "/about-us", value: 1800 },
|
|
452
|
+
{ id: "/blog/article-1", value: 1200 },
|
|
453
|
+
{ id: "/contact", value: 950 },
|
|
454
|
+
];
|
|
455
|
+
return {
|
|
456
|
+
options: { chart: { type: "bar" as const } },
|
|
457
|
+
data: data || defaultData,
|
|
458
|
+
keys: ["value"],
|
|
459
|
+
indexBy: "id",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function getDeviceData(
|
|
464
|
+
data?: Array<{ name: string; visitors: number; percentage: string }>,
|
|
465
|
+
) {
|
|
466
|
+
const defaultData = [
|
|
467
|
+
{ name: "Chrome", visitors: 4500, percentage: "60%", icon: "chrome.svg" },
|
|
468
|
+
{ name: "Safari", visitors: 1500, percentage: "20%", icon: "safari.svg" },
|
|
469
|
+
{ name: "Firefox", visitors: 900, percentage: "12%", icon: "firefox.svg" },
|
|
470
|
+
{ name: "Edge", visitors: 600, percentage: "8%", icon: "edge.svg" },
|
|
471
|
+
];
|
|
472
|
+
return data || defaultData;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function getGoalConversionData(
|
|
476
|
+
data?: (string | number)[][],
|
|
477
|
+
): TableComponentProps["tableData"] {
|
|
478
|
+
const defaultData = [
|
|
479
|
+
["Account Signup", 1500, 1800, "83.33"],
|
|
480
|
+
["Newsletter Subscription", 800, 950, "84.21"],
|
|
481
|
+
["Demo Request", 300, 320, "93.75"],
|
|
482
|
+
["Contact Form Submission", 450, 470, "95.74"],
|
|
483
|
+
["Software Download", 600, 680, "88.24"],
|
|
484
|
+
];
|
|
485
|
+
return {
|
|
486
|
+
title: "Goal Conversions",
|
|
487
|
+
headers: ["Goal", "Uniques", "Total", "CR (%)"],
|
|
488
|
+
rows: data || defaultData,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function cleanReferer(referer: string) {
|
|
493
|
+
if (!referer) return "Direct";
|
|
494
|
+
if (referer === "") return "Direct";
|
|
495
|
+
return referer.replace(/https?:\/\//, "").replace(/\/.*/, "");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function getLocationsMapData(): Array<{ id: string; value: number }> {
|
|
499
|
+
return [{ id: "CAN", value: 300 }];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function getReferrersData(
|
|
503
|
+
data?: Array<{ id: string; value: number }>,
|
|
504
|
+
): NivoPieChartData {
|
|
505
|
+
const defaultData = [
|
|
506
|
+
{ id: "Google", value: 44 },
|
|
507
|
+
{ id: "Direct", value: 55 },
|
|
508
|
+
{ id: "Facebook", value: 13 },
|
|
509
|
+
{ id: "Twitter", value: 43 },
|
|
510
|
+
{ id: "LinkedIn", value: 22 },
|
|
511
|
+
];
|
|
512
|
+
return {
|
|
513
|
+
options: { chart: { type: "pie" as const } },
|
|
514
|
+
data: data || defaultData,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
export type DashboardResponseData = {
|
|
518
|
+
noSiteRecordsExist: boolean;
|
|
519
|
+
ScoreCards: Array<ScorecardProps>;
|
|
520
|
+
PageViewsData: NivoLineChartData;
|
|
521
|
+
EventTypesData: ReturnType<typeof getEventTypesData>;
|
|
522
|
+
DeviceGeoData: ReturnType<typeof getDeviceGeoData>;
|
|
523
|
+
ReferrersData: NivoPieChartData;
|
|
524
|
+
TopPagesData: NivoBarChartData;
|
|
525
|
+
TopSourcesData: ReturnType<typeof getTopSourcesData>;
|
|
526
|
+
BrowserData: ReturnType<typeof getDeviceData>;
|
|
527
|
+
OSData: ReturnType<typeof getDeviceData>;
|
|
528
|
+
Countries?: Array<{ id: string; value: number }>;
|
|
529
|
+
CountryUniques?: Array<{ id: string; value: number }>;
|
|
530
|
+
Pagination: Pagination;
|
|
531
|
+
Regions?: Array<{ id: string; value: number }>;
|
|
532
|
+
EventSummary?: {
|
|
533
|
+
summary: Array<{ event: string | null; count: number; firstSeen: string | null; lastSeen: string | null }>;
|
|
534
|
+
pagination: { offset: number; limit: number; total: number; hasMore: boolean };
|
|
535
|
+
totalEvents: number;
|
|
536
|
+
totalEventTypes: number;
|
|
537
|
+
siteId: number | null;
|
|
538
|
+
dateRange: { start?: string; end?: string };
|
|
539
|
+
} | null;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
export type EventTypeDistributionItem = {
|
|
543
|
+
id: string;
|
|
544
|
+
label: string;
|
|
545
|
+
value: number;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
export const prettifyEventName = (
|
|
549
|
+
name: string,
|
|
550
|
+
labelsMap?: Map<string, string>,
|
|
551
|
+
): string => {
|
|
552
|
+
const custom = labelsMap?.get(name);
|
|
553
|
+
if (custom) return custom;
|
|
554
|
+
|
|
555
|
+
if (name.startsWith("$ac_")) {
|
|
556
|
+
const parts = name.split("_");
|
|
557
|
+
const text = parts[2] || "unnamed";
|
|
558
|
+
const id = parts[3] || null;
|
|
559
|
+
return id ? `${text}_${id}` : text;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return name
|
|
563
|
+
.replace(/_/g, " ")
|
|
564
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
export const getEventTypesDistribution = (
|
|
568
|
+
rows: NonNullable<TableComponentProps["tableData"]>["rows"] | undefined,
|
|
569
|
+
labelsMap?: Map<string, string>,
|
|
570
|
+
): EventTypeDistributionItem[] => {
|
|
571
|
+
const safeRows = rows || [];
|
|
572
|
+
|
|
573
|
+
const filtered = safeRows
|
|
574
|
+
.filter((row) => String(row?.[0] ?? "").toLowerCase() !== "page_view")
|
|
575
|
+
.map((row, index) => {
|
|
576
|
+
const rawName = String(row?.[0] ?? `Step ${index + 1}`);
|
|
577
|
+
const label = prettifyEventName(rawName, labelsMap);
|
|
578
|
+
const value = Number(row?.[1]) || 0;
|
|
579
|
+
return { id: label, label, value };
|
|
580
|
+
})
|
|
581
|
+
.filter((item) => item.value > 0)
|
|
582
|
+
.toSorted((a, b) => b.value - a.value)
|
|
583
|
+
.slice(0, 5);
|
|
584
|
+
|
|
585
|
+
const total = filtered.reduce((sum, item) => sum + item.value, 0);
|
|
586
|
+
return filtered.map((item) => ({
|
|
587
|
+
...item,
|
|
588
|
+
value: total > 0 ? Math.round((item.value / total) * 100) : 0,
|
|
589
|
+
}));
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
export type DeviceGeoData = ReturnType<typeof getDeviceGeoData>;
|
|
594
|
+
export type TopSourcesData = ReturnType<typeof getTopSourcesData>;
|
|
595
|
+
export type BrowserData = ReturnType<typeof getDeviceData>;
|
package/db/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { getDashboardData } from "@db/postgres/sites";
|
|
2
|
+
import type { DBAdapter, EventSelect } from "@db/d1/schema";
|
|
3
|
+
import type { d1_client } from "@db/d1/client";
|
|
4
|
+
import type { pg_client } from "@db/postgres/client";
|
|
5
|
+
import type { DashboardOptions } from "@db/durable/types";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type D1Client = typeof d1_client;
|
|
9
|
+
export type PGClient = ReturnType<typeof pg_client>;
|
|
10
|
+
|
|
11
|
+
//TODO: move implementation over
|
|
12
|
+
type SingleStoreClient = any;
|
|
13
|
+
|
|
14
|
+
export type AdapterToClient = {
|
|
15
|
+
"sqlite": D1Client;
|
|
16
|
+
"postgres": PGClient;
|
|
17
|
+
//WARNING: This is not a real client
|
|
18
|
+
"singlestore": SingleStoreClient;
|
|
19
|
+
"analytics_engine": SingleStoreClient;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type UserRole = "viewer" | "editor" | "admin";
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export type Pagination = {
|
|
26
|
+
limit: number;
|
|
27
|
+
offset: number;
|
|
28
|
+
total: number;
|
|
29
|
+
hasMore: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Client utilities for communicating with Site Durable Objects
|
|
34
|
+
*
|
|
35
|
+
* These functions handle the communication between the main worker
|
|
36
|
+
* and the site-specific durable objects using RPC calls instead of fetch.
|
|
37
|
+
*/
|
|
38
|
+
export interface DashboardDataResult {
|
|
39
|
+
//TODO: make this typed
|
|
40
|
+
events: Array<Partial<EventSelect>>;
|
|
41
|
+
error: boolean;
|
|
42
|
+
pagination: Pagination;
|
|
43
|
+
site_id: number;
|
|
44
|
+
site_uuid: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AdapterResult<T extends DBAdapter> {
|
|
48
|
+
adapter: T;
|
|
49
|
+
client: AdapterToClient[T] | null;
|
|
50
|
+
noSiteRecordsExist: boolean;
|
|
51
|
+
query: DashboardDataResult | null;
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
export type DasboardDataResult = Awaited<ReturnType<typeof getDashboardData>["query"]>;
|
|
55
|
+
export type { DBAdapter, DashboardOptions };
|