Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions convex/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,15 @@ export const getErrorDetails = query({
});

/** Fetch aggregated stats for a single machine. */
function pii<T>(value: T, identity: unknown): T | null {
return identity ? (value ?? null) : null;
}

export const getMachineStats = query({
args: { machineId: v.string() },
handler: async (ctx, { machineId }) => {
const identity = await ctx.auth.getUserIdentity();

const machine = await ctx.db
.query("machines")
.withIndex("by_machineId", (q) => q.eq("machineId", machineId))
Expand Down Expand Up @@ -280,6 +286,9 @@ export const getMachineStats = query({
region: machine.region ?? null,
city: machine.city ?? null,
screen: machine.screen ?? null,
userEmail: pii(machine.userEmail, identity),
userName: pii(machine.userName, identity),
authProvider: pii(machine.authProvider, identity),
},
stats: {
totalSessions,
Expand Down Expand Up @@ -346,6 +355,7 @@ export const getTopPages = query({
export const getPageVisitors = query({
args: { url: v.string(), startDate: v.string(), endDate: v.string() },
handler: async (ctx, { url, startDate, endDate }) => {
const identity = await ctx.auth.getUserIdentity();
const dayStart = new Date(`${startDate}T00:00:00.000Z`).getTime();
const dayEnd = new Date(`${endDate}T23:59:59.999Z`).getTime();

Expand Down Expand Up @@ -381,6 +391,9 @@ export const getPageVisitors = query({
country: machine?.country ?? null,
platform: machine?.platform ?? null,
userAgent: machine?.userAgent ?? null,
userEmail: pii(machine?.userEmail, identity),
userName: pii(machine?.userName, identity),
authProvider: pii(machine?.authProvider, identity),
};
})
);
Expand Down Expand Up @@ -422,6 +435,7 @@ export const getPageViewsOverTime = query({
export const searchMachines = query({
args: { prefix: v.string() },
handler: async (ctx, { prefix }) => {
const identity = await ctx.auth.getUserIdentity();
if (!prefix || prefix.trim().length === 0) return [];
const results = await ctx.db
.query("machines")
Expand All @@ -433,6 +447,9 @@ export const searchMachines = query({
country: m.country ?? null,
platform: m.platform ?? null,
lastSeenAt: m.lastSeenAt,
userEmail: pii(m.userEmail, identity),
userName: pii(m.userName, identity),
authProvider: pii(m.authProvider, identity),
}));
},
});
Expand Down
31 changes: 29 additions & 2 deletions convex/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async function upsertMachine(
timestamp: number;
meta?: Record<string, unknown>;
geo?: { ip?: string; country?: string; region?: string; city?: string };
userInfo?: { email?: string; name?: string; provider?: string };
}
) {
const existing = await ctx.db
Expand All @@ -51,6 +52,9 @@ async function upsertMachine(
country: args.geo?.country,
region: args.geo?.region,
city: args.geo?.city,
userEmail: args.userInfo?.email,
userName: args.userInfo?.name,
authProvider: args.userInfo?.provider,
});
return { isNewMachine: true };
}
Expand All @@ -62,6 +66,9 @@ async function upsertMachine(
platform: args.meta?.platform ?? existing.platform,
referrer: args.meta?.referrer ?? existing.referrer,
screen: args.meta?.screen ?? existing.screen,
userEmail: args.userInfo?.email ?? existing.userEmail,
userName: args.userInfo?.name ?? existing.userName,
authProvider: args.userInfo?.provider ?? existing.authProvider,
});
return { isNewMachine: false };
}
Expand Down Expand Up @@ -122,20 +129,38 @@ async function upsertSession(
}
}

function buildUserInfoMap(events: Array<{ machineId: string; name: string; payload?: unknown }>): Map<string, { email?: string; name?: string; provider?: string }> {
const map = new Map<string, { email?: string; name?: string; provider?: string }>();
for (const event of events) {
if (event.name !== "session_identify") continue;
if (!event.payload || typeof event.payload !== "object") continue;
const p = event.payload as Record<string, unknown>;
const email = typeof p.email === "string" ? p.email : undefined;
const name = typeof p.name === "string" ? p.name : undefined;
const provider = typeof p.provider === "string" ? p.provider : undefined;
if (email || name || provider) {
map.set(event.machineId, { email, name, provider });
}
}
return map;
}

export const recordBatch = mutation({
args: { events: v.array(eventValidator) },
handler: async (ctx, { events }) => {
// Process oldest-first so ordering-dependent bookkeeping (session creation, counters) is correct.
const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp);
const userInfoMap = buildUserInfoMap(sorted);

for (const event of sorted) {
const isBookkeeping = event.name === "session_start";
const isBookkeeping = event.name === "session_start" || event.name === "session_identify";

await upsertMachine(ctx, {
machineId: event.machineId,
userId: event.userId,
timestamp: event.timestamp,
meta: isBookkeeping ? event.payload : undefined,
userInfo: userInfoMap.get(event.machineId),
});

await upsertSession(ctx, {
Expand Down Expand Up @@ -211,16 +236,18 @@ export const recordBatchWithGeo = internalMutation({
},
handler: async (ctx, { events, geo }) => {
const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp);
const userInfoMap = buildUserInfoMap(sorted);

for (const event of sorted) {
const isBookkeeping = event.name === "session_start";
const isBookkeeping = event.name === "session_start" || event.name === "session_identify";

await upsertMachine(ctx, {
machineId: event.machineId,
userId: event.userId,
timestamp: event.timestamp,
meta: isBookkeeping ? event.payload : undefined,
geo,
userInfo: userInfoMap.get(event.machineId),
});

await upsertSession(ctx, {
Expand Down
3 changes: 3 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export default defineSchema({
region: v.optional(v.string()),
city: v.optional(v.string()),
screen: v.optional(v.string()),
userEmail: v.optional(v.string()),
userName: v.optional(v.string()),
authProvider: v.optional(v.string()),
})
.index("by_machineId", ["machineId"])
.index("by_userId", ["userId"])
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@radix-ui/react-use-layout-effect": "^1.1.1",
"@radix-ui/react-use-previous": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.2.4",
"@renderdragonorg/wisp": "^0.1.1",
"@renderdragonorg/wisp": "^0.2.0",
"@sentry/react": "^10.38.0",
"@supabase/supabase-js": "^2.95.3",
"@tabler/icons-react": "^3.36.1",
Expand Down
Loading
Loading