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
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>;
|