Skip to content

Commit de44c81

Browse files
authored
add posthog events on various user actions (#208)
* add page view event support * add posthog events * nit: remove unused import * feedback
1 parent ce52f65 commit de44c81

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+886
-244
lines changed

packages/backend/src/connectionManager.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import os from 'os';
77
import { Redis } from 'ioredis';
88
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
99
import { BackendError, BackendException } from "@sourcebot/error";
10+
import { captureEvent } from "./posthog.js";
1011

1112
interface IConnectionManager {
1213
scheduleConnectionSync: (connection: Connection) => Promise<void>;
@@ -22,6 +23,10 @@ type JobPayload = {
2223
config: ConnectionConfig,
2324
};
2425

26+
type JobResult = {
27+
repoCount: number
28+
}
29+
2530
export class ConnectionManager implements IConnectionManager {
2631
private worker: Worker;
2732
private queue: Queue<JobPayload>;
@@ -217,10 +222,14 @@ export class ConnectionManager implements IConnectionManager {
217222
const totalUpsertDuration = performance.now() - totalUpsertStart;
218223
this.logger.info(`Upserted ${repoData.length} repos in ${totalUpsertDuration}ms`);
219224
});
225+
226+
return {
227+
repoCount: repoData.length,
228+
};
220229
}
221230

222231

223-
private async onSyncJobCompleted(job: Job<JobPayload>) {
232+
private async onSyncJobCompleted(job: Job<JobPayload>, result: JobResult) {
224233
this.logger.info(`Connection sync job ${job.id} completed`);
225234
const { connectionId } = job.data;
226235

@@ -233,14 +242,24 @@ export class ConnectionManager implements IConnectionManager {
233242
syncedAt: new Date()
234243
}
235244
})
245+
246+
captureEvent('backend_connection_sync_job_completed', {
247+
connectionId: connectionId,
248+
repoCount: result.repoCount,
249+
});
236250
}
237251

238252
private async onSyncJobFailed(job: Job | undefined, err: unknown) {
239253
this.logger.info(`Connection sync job failed with error: ${err}`);
240254
if (job) {
255+
const { connectionId } = job.data;
256+
257+
captureEvent('backend_connection_sync_job_failed', {
258+
connectionId: connectionId,
259+
error: err instanceof BackendException ? err.code : 'UNKNOWN',
260+
});
241261

242262
// We may have pushed some metadata during the execution of the job, so we make sure to not overwrite the metadata here
243-
const { connectionId } = job.data;
244263
let syncStatusMetadata: Record<string, unknown> = (await this.db.connection.findUnique({
245264
where: { id: connectionId },
246265
select: { syncStatusMetadata: true }

packages/backend/src/posthogEvents.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ export type PosthogEventMap = {
55
vcs: string;
66
codeHost?: string;
77
},
8-
repo_synced: {
9-
vcs: string;
10-
codeHost?: string;
11-
fetchDuration_s?: number;
12-
cloneDuration_s?: number;
13-
indexDuration_s?: number;
14-
},
158
repo_deleted: {
169
vcs: string;
1710
codeHost?: string;
18-
}
11+
},
12+
//////////////////////////////////////////////////////////////////
13+
backend_connection_sync_job_failed: {
14+
connectionId: number,
15+
error: string,
16+
},
17+
backend_connection_sync_job_completed: {
18+
connectionId: number,
19+
repoCount: number,
20+
},
21+
//////////////////////////////////////////////////////////////////
1922
}
2023

2124
export type PosthogEvent = keyof PosthogEventMap;

packages/backend/src/repoManager.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { cloneRepository, fetchRepository } from "./git.js";
1010
import { existsSync, rmSync, readdirSync } from 'fs';
1111
import { indexGitRepository } from "./zoekt.js";
1212
import os from 'os';
13+
import { BackendException } from "@sourcebot/error";
1314

1415
interface IRepoManager {
1516
blockingPollLoop: () => void;
@@ -308,14 +309,6 @@ export class RepoManager implements IRepoManager {
308309
indexDuration_s = stats!.indexDuration_s;
309310
fetchDuration_s = stats!.fetchDuration_s;
310311
cloneDuration_s = stats!.cloneDuration_s;
311-
312-
captureEvent('repo_synced', {
313-
vcs: 'git',
314-
codeHost: repo.external_codeHostType,
315-
indexDuration_s,
316-
fetchDuration_s,
317-
cloneDuration_s,
318-
});
319312
}
320313

321314
private async onIndexJobCompleted(job: Job<JobPayload>) {

packages/web/src/actions.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import Ajv from "ajv";
44
import { auth } from "./auth";
5-
import { notAuthenticated, notFound, ServiceError, unexpectedError, orgInvalidSubscription } from "@/lib/serviceError";
5+
import { notAuthenticated, notFound, ServiceError, unexpectedError, orgInvalidSubscription, secretAlreadyExists } from "@/lib/serviceError";
66
import { prisma } from "@/prisma";
77
import { StatusCodes } from "http-status-codes";
88
import { ErrorCode } from "@/lib/errorCodes";
@@ -14,7 +14,7 @@ import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
1414
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, GerritConnectionConfig, ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
1515
import { encrypt } from "@sourcebot/crypto"
1616
import { getConnection, getLinkedRepos } from "./data/connection";
17-
import { ConnectionSyncStatus, Prisma, Invite, OrgRole, Connection, Repo, Org, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
17+
import { ConnectionSyncStatus, Prisma, OrgRole, Connection, Repo, Org, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
1818
import { headers } from "next/headers"
1919
import { getStripe } from "@/lib/stripe"
2020
import { getUser } from "@/data/user";
@@ -184,6 +184,19 @@ export const createSecret = async (key: string, value: string, domain: string):
184184
withOrgMembership(session, domain, async ({ orgId }) => {
185185
try {
186186
const encrypted = encrypt(value);
187+
const existingSecret = await prisma.secret.findUnique({
188+
where: {
189+
orgId_key: {
190+
orgId,
191+
key,
192+
}
193+
}
194+
});
195+
196+
if (existingSecret) {
197+
return secretAlreadyExists();
198+
}
199+
187200
await prisma.secret.create({
188201
data: {
189202
orgId,

packages/web/src/app/[domain]/components/configEditor.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import { Button } from "@/components/ui/button";
1919
import { Separator } from "@/components/ui/separator";
2020
import { Schema } from "ajv";
2121
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
22-
22+
import useCaptureEvent from "@/hooks/useCaptureEvent";
23+
import { PosthogEvent, PosthogEventMap } from "@/lib/posthogEvents";
24+
import { CodeHostType } from "@/lib/utils";
2325
export type QuickActionFn<T> = (previous: T) => T;
2426
export type QuickAction<T> = {
2527
name: string;
@@ -29,6 +31,7 @@ export type QuickAction<T> = {
2931

3032
interface ConfigEditorProps<T> {
3133
value: string;
34+
type: CodeHostType;
3235
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3336
onChange: (...event: any[]) => void;
3437
actions: QuickAction<T>[],
@@ -102,8 +105,8 @@ export const isConfigValidJson = (config: string) => {
102105
}
103106

104107
const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCodeMirrorRef>) => {
105-
const { value, onChange, actions, schema } = props;
106-
108+
const { value, type, onChange, actions, schema } = props;
109+
const captureEvent = useCaptureEvent();
107110
const editorRef = useRef<ReactCodeMirrorRef>(null);
108111
useImperativeHandle(
109112
forwardedRef,
@@ -159,6 +162,10 @@ const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCo
159162
disabled={!isConfigValidJson(value)}
160163
onClick={(e) => {
161164
e.preventDefault();
165+
captureEvent('wa_config_editor_quick_action_pressed', {
166+
name,
167+
type,
168+
});
162169
if (editorRef.current?.view) {
163170
onQuickAction(fn, value, editorRef.current.view, {
164171
focusEditor: true,

packages/web/src/app/[domain]/components/connectionCreationForms/secretCombobox.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import githubPatCreation from "@/public/github_pat_creation.png"
3030
import { CodeHostType } from "@/lib/utils";
3131
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
3232
import { isDefined } from '@/lib/utils'
33-
33+
import useCaptureEvent from "@/hooks/useCaptureEvent";
3434
interface SecretComboBoxProps {
3535
isDisabled: boolean;
3636
codeHostType: CodeHostType;
@@ -47,6 +47,7 @@ export const SecretCombobox = ({
4747
const [searchFilter, setSearchFilter] = useState("");
4848
const domain = useDomain();
4949
const [isCreateSecretDialogOpen, setIsCreateSecretDialogOpen] = useState(false);
50+
const captureEvent = useCaptureEvent();
5051

5152
const { data: secrets, isLoading, refetch } = useQuery({
5253
queryKey: ["secrets"],
@@ -154,7 +155,12 @@ export const SecretCombobox = ({
154155
<Button
155156
variant="ghost"
156157
size="sm"
157-
onClick={() => setIsCreateSecretDialogOpen(true)}
158+
onClick={() => {
159+
setIsCreateSecretDialogOpen(true);
160+
captureEvent('wa_secret_combobox_import_secret_pressed', {
161+
type: codeHostType,
162+
});
163+
}}
158164
className={cn(
159165
"w-full justify-start gap-1.5 p-2",
160166
secrets && !isServiceError(secrets) && secrets.length > 0 && "my-2"
@@ -187,10 +193,17 @@ const ImportSecretDialog = ({ open, onOpenChange, onSecretCreated, codeHostType
187193
const [showValue, setShowValue] = useState(false);
188194
const domain = useDomain();
189195
const { toast } = useToast();
196+
const captureEvent = useCaptureEvent();
190197

191198
const formSchema = z.object({
192199
key: z.string().min(1).refine(async (key) => {
193200
const doesSecretExist = await checkIfSecretExists(key, domain);
201+
if(!isServiceError(doesSecretExist)) {
202+
captureEvent('wa_secret_combobox_import_secret_fail', {
203+
type: codeHostType,
204+
error: "A secret with this key already exists.",
205+
});
206+
}
194207
return isServiceError(doesSecretExist) || !doesSecretExist;
195208
}, "A secret with this key already exists."),
196209
value: z.string().min(1),
@@ -211,15 +224,22 @@ const ImportSecretDialog = ({ open, onOpenChange, onSecretCreated, codeHostType
211224
toast({
212225
description: `❌ Failed to create secret`
213226
});
227+
captureEvent('wa_secret_combobox_import_secret_fail', {
228+
type: codeHostType,
229+
error: response.message,
230+
});
214231
} else {
215232
toast({
216233
description: `✅ Secret created successfully!`
217234
});
235+
captureEvent('wa_secret_combobox_import_secret_success', {
236+
type: codeHostType,
237+
});
218238
form.reset();
219239
onOpenChange(false);
220240
onSecretCreated(data.key);
221241
}
222-
}, [domain, toast, onOpenChange, onSecretCreated, form]);
242+
}, [domain, toast, onOpenChange, onSecretCreated, form, codeHostType, captureEvent]);
223243

224244
const codeHostSpecificStep = useMemo(() => {
225245
switch (codeHostType) {

packages/web/src/app/[domain]/components/connectionCreationForms/sharedConnectionCreationForm.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Loader2 } from "lucide-react";
2121
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
2222
import { SecretCombobox } from "./secretCombobox";
2323
import strings from "@/lib/strings";
24+
import useCaptureEvent from "@/hooks/useCaptureEvent";
2425

2526
interface SharedConnectionCreationFormProps<T> {
2627
type: CodeHostType;
@@ -51,7 +52,7 @@ export default function SharedConnectionCreationForm<T>({
5152
const { toast } = useToast();
5253
const domain = useDomain();
5354
const editorRef = useRef<ReactCodeMirrorRef>(null);
54-
55+
const captureEvent = useCaptureEvent();
5556
const formSchema = useMemo(() => {
5657
return z.object({
5758
name: z.string().min(1),
@@ -64,7 +65,7 @@ export default function SharedConnectionCreationForm<T>({
6465
return checkIfSecretExists(secretKey, domain);
6566
}, { message: "Secret not found" }),
6667
});
67-
}, [schema]);
68+
}, [schema, domain]);
6869

6970
const form = useForm<z.infer<typeof formSchema>>({
7071
resolver: zodResolver(formSchema),
@@ -78,13 +79,20 @@ export default function SharedConnectionCreationForm<T>({
7879
toast({
7980
description: `❌ Failed to create connection. Reason: ${response.message}`
8081
});
82+
captureEvent('wa_create_connection_fail', {
83+
type: type,
84+
error: response.message,
85+
});
8186
} else {
8287
toast({
8388
description: `✅ Connection created successfully.`
8489
});
90+
captureEvent('wa_create_connection_success', {
91+
type: type,
92+
});
8593
onCreated?.(response.id);
8694
}
87-
}, [domain, toast, type, onCreated]);
95+
}, [domain, toast, type, onCreated, captureEvent]);
8896

8997
const onConfigChange = useCallback((value: string) => {
9098
form.setValue("config", value);
@@ -168,6 +176,9 @@ export default function SharedConnectionCreationForm<T>({
168176
}
169177
}
170178
},
179+
captureEvent,
180+
"set-secret",
181+
type,
171182
form.getValues("config"),
172183
view,
173184
{
@@ -193,6 +204,7 @@ export default function SharedConnectionCreationForm<T>({
193204
<FormControl>
194205
<ConfigEditor<T>
195206
ref={editorRef}
207+
type={type}
196208
value={value}
197209
onChange={onConfigChange}
198210
actions={quickActions ?? []}

0 commit comments

Comments
 (0)