Skip to content

Commit 14565e3

Browse files
committed
backfilled partner groupid logs
1 parent 40983a1 commit 14565e3

File tree

4 files changed

+213
-6
lines changed

4 files changed

+213
-6
lines changed

apps/web/app/api/activity-logs/route.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@/lib/zod/schemas/activity-log";
77
import { prisma } from "@dub/prisma";
88
import { NextResponse } from "next/server";
9+
import { v4 as uuid } from "uuid";
910
import * as z from "zod/v4";
1011

1112
// GET /api/activity-logs – get activity logs for a resource
@@ -32,5 +33,47 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
3233
},
3334
});
3435

35-
return NextResponse.json(z.array(activityLogSchema).parse(activityLogs));
36+
const parsedActivityLogs = z.array(activityLogSchema).parse(activityLogs);
37+
38+
// polyfill first group change activity log based on program enrollment creation date
39+
if (resourceType === "partner" && resourceId) {
40+
const programEnrollment = await prisma.programEnrollment.findUnique({
41+
where: {
42+
partnerId_programId: {
43+
partnerId: resourceId,
44+
programId,
45+
},
46+
},
47+
select: {
48+
createdAt: true,
49+
groupId: true,
50+
partnerGroup: {
51+
select: {
52+
name: true,
53+
},
54+
},
55+
},
56+
});
57+
if (programEnrollment) {
58+
parsedActivityLogs.push({
59+
id: uuid(),
60+
action: "partner.groupChanged",
61+
description: null,
62+
createdAt: programEnrollment.createdAt,
63+
user: null,
64+
changeSet: {
65+
group: {
66+
old: null,
67+
new: parsedActivityLogs[parsedActivityLogs.length - 1]?.changeSet
68+
?.group.old ?? {
69+
id: programEnrollment.groupId,
70+
name: programEnrollment.partnerGroup?.name,
71+
},
72+
},
73+
},
74+
});
75+
}
76+
}
77+
78+
return NextResponse.json(parsedActivityLogs);
3679
});

apps/web/lib/api/activity-log/track-activity-log.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,17 @@ export const trackActivityLog = async (
4747
}
4848

4949
try {
50-
await prisma.activityLog.createMany({
50+
const createdActivityLogs = await prisma.activityLog.createMany({
5151
data: inputs.map((input) => ({
5252
...input,
5353
changeSet: input.changeSet as Prisma.InputJsonValue,
5454
})),
5555
});
5656

57-
console.log("[trackActivityLog] Activity log created", prettyPrint(inputs));
57+
console.log(
58+
`[trackActivityLog] Created ${createdActivityLogs.count} activity logs`,
59+
prettyPrint(inputs),
60+
);
5861
} catch (error) {
5962
logger.error("[trackActivityLog] Failed to create activity log", error);
6063
await logger.flush();
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { buildProgramEnrollmentChangeSet } from "@/lib/api/activity-log/build-program-enrollment-change-set";
2+
import { trackActivityLog } from "@/lib/api/activity-log/track-activity-log";
3+
import { prisma } from "@dub/prisma";
4+
import { groupBy } from "@dub/utils";
5+
import { differenceInSeconds } from "date-fns";
6+
import "dotenv-flow/config";
7+
import * as fs from "fs";
8+
import * as Papa from "papaparse";
9+
10+
let logsToBackfill: {
11+
programId: string;
12+
partnerId: string;
13+
groupId: string;
14+
timestamp: Date;
15+
}[] = [];
16+
17+
// Remove the following before running this script:
18+
// - "server-only" from serialize-reward.ts
19+
// - import { logger } from "@/lib/axiom/server" from track-activity-log.ts
20+
async function main() {
21+
Papa.parse(fs.createReadStream("backfill_group_id_logs.csv", "utf-8"), {
22+
header: true,
23+
skipEmptyLines: true,
24+
step: (result: {
25+
data: {
26+
program_id: string;
27+
partner_id: string;
28+
partner_group_id: string;
29+
timestamp: string;
30+
};
31+
}) => {
32+
logsToBackfill.push({
33+
programId: result.data.program_id,
34+
partnerId: result.data.partner_id,
35+
groupId: result.data.partner_group_id,
36+
timestamp: new Date(result.data.timestamp),
37+
});
38+
},
39+
complete: async () => {
40+
const programs = await prisma.program.findMany({
41+
select: {
42+
id: true,
43+
slug: true,
44+
workspaceId: true,
45+
workspace: {
46+
select: {
47+
users: {
48+
select: {
49+
userId: true,
50+
},
51+
orderBy: {
52+
createdAt: "asc",
53+
},
54+
take: 1,
55+
},
56+
},
57+
},
58+
},
59+
});
60+
61+
for (const program of programs) {
62+
const filteredLogs = logsToBackfill.filter(
63+
(log) => log.programId === program.id,
64+
);
65+
console.log(
66+
`Found ${filteredLogs.length} logs to backfill for program ${program.slug}`,
67+
);
68+
69+
const groups = await prisma.partnerGroup.findMany({
70+
where: {
71+
id: {
72+
in: filteredLogs.map((log) => log.groupId),
73+
},
74+
},
75+
select: {
76+
id: true,
77+
name: true,
78+
},
79+
});
80+
81+
const groupNameMap = new Map(
82+
groups.map((group) => [group.id, group.name]),
83+
);
84+
85+
const activityLogHistory = await prisma.activityLog.findMany({
86+
where: {
87+
programId: program.id,
88+
resourceType: "partner",
89+
},
90+
});
91+
92+
// group by and partner id and sort by timestamp to show chronological order
93+
const groupedLogs = Object.entries(
94+
groupBy(filteredLogs, (log) => log.partnerId),
95+
).map(([partnerId, logs]) => ({
96+
partnerId,
97+
activityLogsToBackfill: logs
98+
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) // sort by timestamp to show chronological order
99+
.map((log, idx) => {
100+
if (idx === 0) return null;
101+
if (
102+
activityLogHistory.find(
103+
(a) =>
104+
a.resourceId === log.partnerId &&
105+
differenceInSeconds(a.createdAt, log.timestamp) < 10,
106+
)
107+
) {
108+
// console.log(
109+
// `Skipping log for partner ${log.partnerId} because it already exists in the activity log history`,
110+
// );
111+
return null;
112+
}
113+
return {
114+
workspaceId: program.workspaceId,
115+
programId: log.programId,
116+
resourceType: "partner" as const,
117+
resourceId: log.partnerId,
118+
action: "partner.groupChanged" as const,
119+
createdAt: log.timestamp,
120+
userId: program.workspace.users[0].userId,
121+
changeSet: buildProgramEnrollmentChangeSet({
122+
oldEnrollment: {
123+
partnerGroup: groupNameMap.get(logs[idx - 1].groupId)
124+
? {
125+
id: logs[idx - 1].groupId,
126+
name: groupNameMap.get(logs[idx - 1].groupId)!,
127+
}
128+
: null,
129+
},
130+
newEnrollment: groupNameMap.get(log.groupId)
131+
? {
132+
partnerGroup: groupNameMap.get(log.groupId)
133+
? {
134+
id: log.groupId,
135+
name: groupNameMap.get(log.groupId)!,
136+
}
137+
: null,
138+
}
139+
: null,
140+
}),
141+
};
142+
})
143+
.filter((log): log is NonNullable<typeof log> => log !== null),
144+
}));
145+
146+
const withChanges = groupedLogs.filter(
147+
(group) => group.activityLogsToBackfill.length,
148+
);
149+
console.log(
150+
`Found ${withChanges.length} partners with activity logs to backfill`,
151+
);
152+
153+
await trackActivityLog(
154+
withChanges.flatMap((group) => group.activityLogsToBackfill),
155+
);
156+
}
157+
},
158+
});
159+
}
160+
161+
main();

apps/web/ui/activity-logs/action-renderers/partner-group-changed-renderer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@ export function PartnerGroupChangedRenderer({ log }: { log: ActivityLog }) {
3030

3131
return (
3232
<>
33-
<Label>Moved to</Label>
33+
<Label>{groupChange.old ? "Moved to" : "Added to"}</Label>
3434
<GroupPill name={newGroup.name} color={newGroupColor} />
3535
{log.user ? (
3636
<>
3737
<Label>by</Label>
3838
<UserChip user={log.user} />
3939
</>
40-
) : (
40+
) : groupChange.old ? (
4141
<>
4242
<Label>automatically by</Label>
4343
<SourcePill icon={<Bolt className="size-3" />} label="Group move" />
4444
</>
45-
)}
45+
) : null}
4646
</>
4747
);
4848
}

0 commit comments

Comments
 (0)