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.
Files changed (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
package/db/d1/sites.ts ADDED
@@ -0,0 +1,374 @@
1
+ import { d1_client } from "@db/d1/client";
2
+ import { team_member, team, sites, siteEvents, invited_user, type SiteInsert } from "@db/d1/schema";
3
+ import { createId } from "@paralleldrive/cuid2";
4
+ import { and, asc, count, eq, gte, lte } from "drizzle-orm";
5
+ import type { WebEvent } from "@/templates/trackWebEvents";
6
+ import type { AuthUserSession } from "@lib/auth";
7
+ import { DashboardOptions } from "@db/types";
8
+ import { IS_DEV } from "rwsdk/constants";
9
+
10
+ export async function createNewAccount(user: AuthUserSession["user"]) {
11
+ const normalized_email = user.email.trim().toLowerCase();
12
+
13
+ const [pending_invite] = await d1_client
14
+ .select()
15
+ .from(invited_user)
16
+ .where(and(eq(invited_user.email, normalized_email), eq(invited_user.accepted, false)))
17
+ .limit(1);
18
+
19
+ if (pending_invite) {
20
+ const [teamMember] = await d1_client
21
+ .insert(team_member)
22
+ .values({
23
+ team_id: pending_invite.team_id,
24
+ user_id: user.id,
25
+ role: pending_invite.role ?? "editor",
26
+ })
27
+ .onConflictDoNothing({ target: [team_member.team_id, team_member.user_id] })
28
+ .returning();
29
+
30
+ await d1_client
31
+ .update(invited_user)
32
+ .set({ accepted: true })
33
+ .where(eq(invited_user.id, pending_invite.id));
34
+
35
+ if (!teamMember) {
36
+ const [existingMembership] = await d1_client
37
+ .select()
38
+ .from(team_member)
39
+ .where(
40
+ and(
41
+ eq(team_member.team_id, pending_invite.team_id),
42
+ eq(team_member.user_id, user.id),
43
+ ),
44
+ )
45
+ .limit(1);
46
+
47
+ return { newTeam: null, teamMember: existingMembership ?? null };
48
+ }
49
+
50
+ return { newTeam: null, teamMember };
51
+ }
52
+
53
+ const [primary_team] = await d1_client
54
+ .select({ id: team.id })
55
+ .from(team)
56
+ .orderBy(asc(team.id))
57
+ .limit(1);
58
+
59
+ if (primary_team) {
60
+ const [teamMember] = await d1_client
61
+ .insert(team_member)
62
+ .values({
63
+ team_id: primary_team.id,
64
+ user_id: user.id,
65
+ role: "viewer",
66
+ })
67
+ .onConflictDoNothing({ target: [team_member.team_id, team_member.user_id] })
68
+ .returning();
69
+
70
+ if (teamMember) return { newTeam: null, teamMember };
71
+
72
+ const [existingMembership] = await d1_client
73
+ .select()
74
+ .from(team_member)
75
+ .where(
76
+ and(
77
+ eq(team_member.team_id, primary_team.id),
78
+ eq(team_member.user_id, user.id),
79
+ ),
80
+ )
81
+ .limit(1);
82
+
83
+ return { newTeam: null, teamMember: existingMembership ?? null };
84
+ }
85
+
86
+ try {
87
+ const [newTeam] = await d1_client
88
+ .insert(team)
89
+ .values({ created_by: user.id })
90
+ .returning();
91
+
92
+ const [first_team_now] = await d1_client
93
+ .select({ id: team.id })
94
+ .from(team)
95
+ .orderBy(asc(team.id))
96
+ .limit(1);
97
+
98
+ if (first_team_now && first_team_now.id !== newTeam.id) {
99
+ await d1_client.delete(team).where(eq(team.id, newTeam.id));
100
+
101
+ const [teamMember] = await d1_client
102
+ .insert(team_member)
103
+ .values({
104
+ team_id: first_team_now.id,
105
+ user_id: user.id,
106
+ role: "viewer",
107
+ })
108
+ .onConflictDoNothing({ target: [team_member.team_id, team_member.user_id] })
109
+ .returning();
110
+
111
+ if (teamMember) return { newTeam: null, teamMember };
112
+
113
+ const [existingMembership] = await d1_client
114
+ .select()
115
+ .from(team_member)
116
+ .where(
117
+ and(
118
+ eq(team_member.team_id, first_team_now.id),
119
+ eq(team_member.user_id, user.id),
120
+ ),
121
+ )
122
+ .limit(1);
123
+
124
+ return { newTeam: null, teamMember: existingMembership ?? null };
125
+ }
126
+
127
+ const [teamMember] = await d1_client.insert(team_member).values({
128
+ team_id: newTeam.id,
129
+ user_id: user.id,
130
+ role: "admin",
131
+ }).returning();
132
+
133
+ return { newTeam, teamMember };
134
+ } catch (e) {
135
+ if (IS_DEV) console.log("🔥🔥🔥 createNewAccount error", e);
136
+ return null;
137
+ }
138
+ }
139
+
140
+ export async function getSitesForUser(user_id: string, preferredTeamId?: number | null) {
141
+ try {
142
+ const userTeams = await d1_client
143
+ .select({
144
+ team_id: team_member.team_id,
145
+ role: team_member.role,
146
+ allowed_site_ids: team_member.allowed_site_ids,
147
+ db_adapter: team.db_adapter,
148
+ name: team.name, external_id: team.external_id,
149
+ })
150
+ .from(team_member)
151
+ .leftJoin(team, eq(team.id, team_member.team_id))
152
+ .where(eq(team_member.user_id, user_id));
153
+
154
+ if (userTeams.length === 0) return null;
155
+ const preferredTeam =
156
+ preferredTeamId != null
157
+ ? userTeams.find((team) => team.team_id === preferredTeamId)
158
+ : null;
159
+ const userTeam = preferredTeam ?? userTeams[0];
160
+ const allSites = await d1_client
161
+ .select()
162
+ .from(sites)
163
+ .where(eq(sites.team_id, userTeam.team_id));
164
+
165
+ const allowedSiteIds = userTeam.allowed_site_ids ?? ["all"];
166
+ const sitesList = allowedSiteIds.includes("all")
167
+ ? allSites
168
+ : allSites.filter((site) => allowedSiteIds.includes(site.site_id));
169
+ const normalizedSites = sitesList.map((site) => ({
170
+ ...site,
171
+ event_load_strategy: site.event_load_strategy ?? "sdk",
172
+ gdpr: site.gdpr ?? false,
173
+ }));
174
+
175
+ const remainingTeams = userTeams.filter(
176
+ (team) => team.team_id !== userTeam.team_id,
177
+ );
178
+
179
+ return {
180
+ sitesList: normalizedSites,
181
+ teamHasSites: allSites.length > 0,
182
+ all_teams: remainingTeams,
183
+ team: {
184
+ id: userTeam.team_id,
185
+ role: userTeam.role,
186
+ db_adapter: userTeam.db_adapter,
187
+ external_id: userTeam.external_id,
188
+ name: userTeam.name
189
+ }
190
+ };
191
+ } catch (e) {
192
+ if (IS_DEV) console.log('🔥🔥🔥 getSiteCount error', e);
193
+ return null;
194
+ }
195
+ }
196
+
197
+ export async function checkIfSiteEventsHaveRows(site_id: number | string) {
198
+ const whereFilters = [];
199
+ if (typeof site_id === 'string') {
200
+ whereFilters.push(eq(siteEvents.tag_id, site_id));
201
+ }
202
+ else {
203
+ whereFilters.push(eq(siteEvents.site_id, site_id));
204
+ }
205
+ const siteEv = await d1_client
206
+ .select({ id: siteEvents.id })
207
+ .from(siteEvents)
208
+ .where(and(...whereFilters))
209
+ .limit(1);
210
+ if (siteEv.length == 0) return true;
211
+ return false;
212
+ }
213
+
214
+
215
+ export async function getDashboardData(options: DashboardOptions) {
216
+ if (IS_DEV) console.log("🔥🔥🔥 getDashboardData", options);
217
+ const { date, site_id, team_id } = options;
218
+ const query = d1_client.select({
219
+ page_url: siteEvents.page_url,
220
+ client_page_url: siteEvents.client_page_url,
221
+ referer: siteEvents.referer,
222
+ event: siteEvents.event,
223
+ createdAt: siteEvents.createdAt,
224
+ operating_system: siteEvents.operating_system,
225
+ browser: siteEvents.browser,
226
+ country: siteEvents.country,
227
+ region: siteEvents.region,
228
+ city: siteEvents.city,
229
+ rid: siteEvents.rid,
230
+ postal: siteEvents.postal,
231
+ screen_width: siteEvents.screen_width,
232
+ screen_height: siteEvents.screen_height,
233
+ device_type: siteEvents.device_type,
234
+ }).from(siteEvents);
235
+ const whereFilters = [eq(siteEvents.team_id, team_id)];
236
+ if (typeof site_id === 'string') {
237
+ whereFilters.push(eq(siteEvents.tag_id, site_id));
238
+ }
239
+ else {
240
+ whereFilters.push(eq(siteEvents.site_id, site_id));
241
+ }
242
+ if (date) {
243
+ if (date.start) {
244
+ // whereFilters.push(sql`${siteEvents.createdAt}/1000 >= ${date.start.getTime() / 1000}`)
245
+ whereFilters.push(gte(siteEvents.createdAt, date.start));
246
+ }
247
+ if (date.end) {
248
+ // Set end date to end of day (23:59:59.999) to include the entire day
249
+ const endOfDay = new Date(date.end);
250
+ endOfDay.setHours(23, 59, 59, 999);
251
+ // whereFilters.push(sql`${siteEvents.createdAt}/1000 <= ${date.end.getTime() / 1000}`)
252
+ whereFilters.push(lte(siteEvents.createdAt, endOfDay));
253
+ }
254
+ } else {
255
+ // Default to last 7 days
256
+ const sevenDaysAgo = new Date();
257
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
258
+ // whereFilters.push(sql`${siteEvents.createdAt}/1000 >= ${sevenDaysAgo.getTime() / 1000}`)
259
+ whereFilters.push(gte(siteEvents.createdAt, sevenDaysAgo));
260
+ }
261
+
262
+ const noSiteRecordsExist = checkIfSiteEventsHaveRows(site_id)
263
+ query.where(and(...whereFilters));
264
+ if (IS_DEV) console.log(query.toSQL())
265
+ return { query, client: null, noSiteRecordsExist };
266
+
267
+ }
268
+
269
+ export async function createSite(data: SiteInsert) {
270
+ const { name, domain, track_web_events, gdpr, team_id, event_load_strategy } = data;
271
+ if (!team_id) throw new Error('team_id is required');
272
+ try {
273
+ const [newSite] = await d1_client.insert(sites).values({
274
+ name,
275
+ domain,
276
+ track_web_events,
277
+ event_load_strategy,
278
+ gdpr,
279
+ team_id
280
+ }).returning();
281
+ return newSite;
282
+ } catch (e) {
283
+ if (IS_DEV) console.log('🔥🔥🔥 createSite error', e);
284
+ return null;
285
+ }
286
+ }
287
+
288
+
289
+ //WARNING: THIS IS ONLY FOR LOADING INTO THE TAG DO NOT USE THIS FOR ANYTHING ELSE
290
+ export async function getSiteForTag(account: string) {
291
+ const [site] = await d1_client
292
+ .select()
293
+ .from(sites)
294
+ .where(eq(sites.tag_id, account))
295
+ .limit(1);
296
+ return site;
297
+ }
298
+
299
+
300
+ export async function insertSiteEvent(event: WebEvent) {
301
+ const [newEvent] = await d1_client.insert(siteEvents).values({
302
+ event: event.event!,
303
+ tag_id: event.tag_id!,
304
+ client_page_url: event.client_page_url,
305
+ screen_height: event.screen_height,
306
+ screen_width: event.screen_width,
307
+ rid: event.rid,
308
+ browser: event.browser,
309
+ operating_system: event.operating_system,
310
+ device_type: event.device_type,
311
+ custom_data: event.custom_data,
312
+ country: event.country,
313
+ region: event.region,
314
+ city: event.city,
315
+ postal: event.postal,
316
+ site_id: event.site_id!,
317
+ page_url: event.page_url,
318
+ bot_data: event.bot_data,
319
+ query_params: event.query_params,
320
+ referer: event.referer,
321
+ team_id: event.account_id,
322
+
323
+ }).returning();
324
+ return newEvent;
325
+
326
+ }
327
+
328
+ export type SiteRidConfig = {
329
+ site_id: number;
330
+ rid_salt: string | null;
331
+ rid_salt_expire: Date | null;
332
+ };
333
+
334
+ function buildRidSaltExpireDate(base = new Date()): Date {
335
+ const date = new Date(base);
336
+ date.setDate(date.getDate() + 30);
337
+ return date;
338
+ }
339
+
340
+ export async function getSiteRidConfig(site_id: number): Promise<SiteRidConfig | null> {
341
+ const [site] = await d1_client
342
+ .select({
343
+ site_id: sites.site_id,
344
+ rid_salt: sites.rid_salt,
345
+ rid_salt_expire: sites.rid_salt_expire,
346
+ })
347
+ .from(sites)
348
+ .where(eq(sites.site_id, site_id))
349
+ .limit(1);
350
+ return site ?? null;
351
+ }
352
+
353
+ export async function rotateSiteRidSalt(site_id: number): Promise<SiteRidConfig | null> {
354
+ const nextSalt = createId();
355
+ const nextExpire = buildRidSaltExpireDate();
356
+ const [updated] = await d1_client
357
+ .update(sites)
358
+ .set({
359
+ rid_salt: nextSalt,
360
+ rid_salt_expire: nextExpire,
361
+ updatedAt: new Date(),
362
+ })
363
+ .where(eq(sites.site_id, site_id))
364
+ .returning({
365
+ site_id: sites.site_id,
366
+ rid_salt: sites.rid_salt,
367
+ rid_salt_expire: sites.rid_salt_expire,
368
+ });
369
+ return updated ?? null;
370
+ }
371
+
372
+ export type GetSitesForUser = Awaited<ReturnType<typeof getSitesForUser>>
373
+ export type SitesContext = NonNullable<GetSitesForUser>["sitesList"];
374
+ export type TeamContext = Omit<NonNullable<GetSitesForUser>["team"], "role" | "db_adapter">;
@@ -0,0 +1,101 @@
1
+ import { and, eq, gte, lt, sql } from "drizzle-orm";
2
+
3
+ import { d1_client } from "@db/d1/client";
4
+ import { team_ai_usage, type TeamAiUsageInsert } from "@db/d1/schema";
5
+
6
+ export type TeamAiUsageRequestType = NonNullable<TeamAiUsageInsert["request_type"]>;
7
+ export type TeamAiUsageStatus = NonNullable<TeamAiUsageInsert["status"]>;
8
+
9
+ export type TrackTeamAiUsageInput = {
10
+ team_id: number;
11
+ user_id?: string | null;
12
+ site_id?: number | null;
13
+ request_id?: string | null;
14
+ request_type: TeamAiUsageRequestType;
15
+ provider?: string | null;
16
+ model?: string | null;
17
+ status: TeamAiUsageStatus;
18
+ error_code?: string | null;
19
+ error_message?: string | null;
20
+ input_tokens?: number | null;
21
+ output_tokens?: number | null;
22
+ total_tokens?: number | null;
23
+ tool_calls?: number | null;
24
+ message_count?: number | null;
25
+ prompt_chars?: number | null;
26
+ completion_chars?: number | null;
27
+ duration_ms?: number | null;
28
+ createdAt?: Date;
29
+ };
30
+
31
+ const toNullableInt = (value: unknown): number | null => {
32
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
33
+ return Math.max(0, Math.floor(value));
34
+ };
35
+
36
+ export async function trackTeamAiUsage(input: TrackTeamAiUsageInput) {
37
+ await d1_client.insert(team_ai_usage).values({
38
+ team_id: input.team_id,
39
+ user_id: input.user_id ?? null,
40
+ site_id: input.site_id ?? null,
41
+ request_id: input.request_id ?? null,
42
+ request_type: input.request_type,
43
+ provider: input.provider ?? null,
44
+ model: input.model ?? null,
45
+ status: input.status,
46
+ error_code: input.error_code ?? null,
47
+ error_message: input.error_message ?? null,
48
+ input_tokens: toNullableInt(input.input_tokens),
49
+ output_tokens: toNullableInt(input.output_tokens),
50
+ total_tokens: toNullableInt(input.total_tokens),
51
+ tool_calls: toNullableInt(input.tool_calls),
52
+ message_count: toNullableInt(input.message_count),
53
+ prompt_chars: toNullableInt(input.prompt_chars),
54
+ completion_chars: toNullableInt(input.completion_chars),
55
+ duration_ms: toNullableInt(input.duration_ms),
56
+ createdAt: input.createdAt ?? new Date(),
57
+ });
58
+ }
59
+
60
+ function getUtcDayWindow(day: Date) {
61
+ const start = new Date(Date.UTC(
62
+ day.getUTCFullYear(),
63
+ day.getUTCMonth(),
64
+ day.getUTCDate(),
65
+ 0,
66
+ 0,
67
+ 0,
68
+ 0,
69
+ ));
70
+ const end = new Date(start);
71
+ end.setUTCDate(end.getUTCDate() + 1);
72
+ return { start, end };
73
+ }
74
+
75
+ export async function getTeamAiUsageForUtcDay(teamId: number, day = new Date()) {
76
+ const { start, end } = getUtcDayWindow(day);
77
+
78
+ const [summary] = await d1_client
79
+ .select({
80
+ requestCount: sql<number>`count(*)`,
81
+ inputTokens: sql<number>`coalesce(sum(${team_ai_usage.input_tokens}), 0)`,
82
+ outputTokens: sql<number>`coalesce(sum(${team_ai_usage.output_tokens}), 0)`,
83
+ totalTokens: sql<number>`coalesce(sum(${team_ai_usage.total_tokens}), 0)`,
84
+ })
85
+ .from(team_ai_usage)
86
+ .where(and(
87
+ eq(team_ai_usage.team_id, teamId),
88
+ eq(team_ai_usage.status, "success"),
89
+ gte(team_ai_usage.createdAt, start),
90
+ lt(team_ai_usage.createdAt, end),
91
+ ));
92
+
93
+ return {
94
+ start,
95
+ end,
96
+ requestCount: summary?.requestCount ?? 0,
97
+ inputTokens: summary?.inputTokens ?? 0,
98
+ outputTokens: summary?.outputTokens ?? 0,
99
+ totalTokens: summary?.totalTokens ?? 0,
100
+ };
101
+ }
package/db/d1/teams.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { d1_client } from "@db/d1/client";
2
+ import { team_member, team, user, api_key, sites, invited_user, Permissions, AllowedMembers } from "@db/d1/schema";
3
+ import { and, desc, eq } from "drizzle-orm";
4
+
5
+ export async function updateTeamName(name: string, id: number) {
6
+ const team_vals = await d1_client
7
+ .update(team)
8
+ .set({ name })
9
+ .where(eq(team.id, id))
10
+ .returning();
11
+ return team_vals;
12
+ }
13
+
14
+ //TODO: Allow multi team
15
+ export async function userExists(email: string, team_id: number) {
16
+ const userDetails = await d1_client
17
+ .select()
18
+ .from(user)
19
+ .where(eq(user.email, email))
20
+ .limit(1);
21
+
22
+ if (userDetails.length > 0) {
23
+ //check if user is in team
24
+ const [teamMember] = await d1_client
25
+ .select()
26
+ .from(team_member)
27
+ .where(eq(team_member.user_id, userDetails[0].id))
28
+ .limit(1);
29
+
30
+ if (teamMember.team_id === team_id) {
31
+ return { create: false, addToTeam: false, userDetails: userDetails[0] };
32
+ } else return { create: false, addToTeam: true, userDetails: userDetails[0] };
33
+ } else return { create: true, addToTeam: true, userDetails: null };
34
+ }
35
+
36
+ export async function getTeamMembers(team_id: number) {
37
+ const team_members = await d1_client
38
+ .select({
39
+ id: team_member.user_id,
40
+ name: user.name,
41
+ email: user.email,
42
+ role: team_member.role,
43
+ allowed_site_ids: team_member.allowed_site_ids,
44
+ })
45
+ .from(team_member)
46
+ .leftJoin(user, eq(team_member.user_id, user.id))
47
+ .where(eq(team_member.team_id, team_id))
48
+ return team_members;
49
+ }
50
+
51
+ export async function addTeamMember(user_id: string, team_id: number) {
52
+ const new_member = await d1_client
53
+ .insert(team_member)
54
+ .values({
55
+ user_id,
56
+ team_id,
57
+ })
58
+ .returning();
59
+ return new_member;
60
+ }
61
+
62
+
63
+ export async function getApiKeys(team_id: number) {
64
+ const keyDetails = await d1_client
65
+ .select()
66
+ .from(api_key)
67
+ .where(eq(api_key.team_id, team_id))
68
+ return keyDetails;
69
+ }
70
+ export async function addApiKey(options: { team_id: number, site_id: number, permissions: Permissions, allowed_team_members?: AllowedMembers }) {
71
+ const { team_id, site_id, permissions } = options;
72
+ let allowed = options.allowed_team_members || ["all"];
73
+ const new_key = await d1_client.insert(api_key).values({
74
+ team_id,
75
+ site_id,
76
+ permissions,
77
+ allowed_team_members: allowed,
78
+ }).returning();
79
+ return new_key;
80
+ }
81
+
82
+ export async function getTeamSites(team_id: number) {
83
+ const team_sites = await d1_client
84
+ .select({
85
+ site_id: sites.site_id,
86
+ name: sites.name,
87
+ domain: sites.domain,
88
+ })
89
+ .from(sites)
90
+ .where(eq(sites.team_id, team_id));
91
+ return team_sites;
92
+ }
93
+
94
+ export async function getTeamPendingInvites(team_id: number) {
95
+ const pendingInvites = await d1_client
96
+ .select({
97
+ id: invited_user.id,
98
+ name: invited_user.name,
99
+ email: invited_user.email,
100
+ role: invited_user.role,
101
+ createdAt: invited_user.createdAt,
102
+ })
103
+ .from(invited_user)
104
+ .where(
105
+ and(
106
+ eq(invited_user.team_id, team_id),
107
+ eq(invited_user.accepted, false),
108
+ ),
109
+ )
110
+ .orderBy(desc(invited_user.createdAt));
111
+
112
+ return pendingInvites;
113
+ }
114
+
115
+ export async function getTeamSettings(team_id: number) {
116
+ const [members, keys, team_sites, pendingInvites] = await Promise.all([
117
+ getTeamMembers(team_id),
118
+ getApiKeys(team_id),
119
+ getTeamSites(team_id),
120
+ getTeamPendingInvites(team_id),
121
+ ]);
122
+ return { members, keys, sites: team_sites, pendingInvites };
123
+ }
124
+
125
+ export type GetTeamMembers = ReturnType<typeof getTeamMembers>;
126
+ export type GetTeamSites = ReturnType<typeof getTeamSites>;
127
+ export type GetTeamSettings = ReturnType<typeof getTeamSettings>;
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ out: './db/durable/migrations',
5
+ schema: './db/durable/schema.ts',
6
+ dialect: 'sqlite',
7
+ driver: 'durable-sqlite',
8
+ });