Skip to content

Commit 6185a67

Browse files
authored
Add new migration for signup tracking (#3569)
1 parent 5cb9f9e commit 6185a67

File tree

6 files changed

+212
-230
lines changed

6 files changed

+212
-230
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
CREATE EXTENSION IF NOT EXISTS http;
2+
CREATE TABLE IF NOT EXISTS public.system_config (
3+
key TEXT PRIMARY KEY,
4+
value TEXT NOT NULL,
5+
description TEXT,
6+
created_at TIMESTAMPTZ DEFAULT NOW()
7+
);
8+
INSERT INTO public.system_config (key, value, description)
9+
VALUES (
10+
'enable_tracking',
11+
'false',
12+
'Enable tracking of user signups'
13+
) ON CONFLICT (key) DO NOTHING;
14+
ALTER TABLE public.system_config ENABLE ROW LEVEL SECURITY;
15+
REVOKE ALL PRIVILEGES ON TABLE public.system_config
16+
FROM anon;
17+
REVOKE ALL PRIVILEGES ON TABLE public.system_config
18+
FROM authenticated;
19+
CREATE POLICY "Allow postgres access to system_config" ON public.system_config FOR ALL TO postgres USING (true);
20+
CREATE OR REPLACE FUNCTION public.track_user_signup_to_posthog() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER
21+
SET search_path = public AS $$
22+
DECLARE posthog_key TEXT;
23+
tracking_enabled TEXT;
24+
response http_response;
25+
BEGIN BEGIN
26+
SELECT value INTO tracking_enabled
27+
FROM public.system_config
28+
WHERE key = 'enable_tracking';
29+
IF tracking_enabled = 'true' THEN BEGIN
30+
SELECT value INTO posthog_key
31+
FROM public.system_config
32+
WHERE key = 'posthog_public_key';
33+
IF posthog_key IS NOT NULL
34+
AND posthog_key != '' THEN
35+
SELECT * INTO response
36+
FROM http_post(
37+
'https://app.posthog.com/capture/',
38+
jsonb_build_object(
39+
'api_key',
40+
posthog_key,
41+
'event',
42+
'user_signed_up',
43+
'properties',
44+
jsonb_build_object(
45+
'distinct_id',
46+
NEW.id,
47+
'email',
48+
NEW.email,
49+
'source',
50+
'database_trigger',
51+
'timestamp',
52+
extract(
53+
epoch
54+
from now()
55+
) * 1000
56+
)
57+
)::text,
58+
'application/json'
59+
);
60+
END IF;
61+
EXCEPTION
62+
WHEN OTHERS THEN NULL;
63+
END;
64+
END IF;
65+
EXCEPTION
66+
WHEN OTHERS THEN NULL;
67+
END;
68+
RETURN NEW;
69+
END;
70+
$$;
71+
CREATE TRIGGER track_user_signup_after_creation
72+
AFTER
73+
INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.track_user_signup_to_posthog();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
CREATE OR REPLACE FUNCTION public.track_organization_onboarding() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$
2+
DECLARE posthog_key TEXT;
3+
tracking_enabled TEXT;
4+
org_owner_email TEXT;
5+
org_owner_name TEXT;
6+
response http_response;
7+
BEGIN IF (
8+
OLD.has_onboarded = false
9+
AND NEW.has_onboarded = true
10+
AND NEW.tier != 'demo'
11+
) THEN
12+
SELECT value INTO tracking_enabled
13+
FROM public.system_config
14+
WHERE key = 'enable_tracking';
15+
IF tracking_enabled = 'true' THEN BEGIN
16+
SELECT email,
17+
COALESCE(
18+
raw_user_meta_data->>'full_name',
19+
raw_user_meta_data->>'name',
20+
''
21+
) INTO org_owner_email,
22+
org_owner_name
23+
FROM auth.users
24+
WHERE id = NEW.owner;
25+
SELECT value INTO posthog_key
26+
FROM public.system_config
27+
WHERE key = 'posthog_public_key';
28+
IF posthog_key IS NOT NULL
29+
AND posthog_key != '' THEN
30+
SELECT * INTO response
31+
FROM http_post(
32+
'https://app.posthog.com/capture/',
33+
jsonb_build_object(
34+
'api_key',
35+
posthog_key,
36+
'event',
37+
'organization_onboarded',
38+
'distinct_id',
39+
NEW.owner,
40+
'properties',
41+
jsonb_build_object(
42+
'email',
43+
org_owner_email,
44+
'name',
45+
org_owner_name,
46+
'timestamp',
47+
extract(
48+
epoch
49+
from now()
50+
) * 1000,
51+
'$groups',
52+
jsonb_build_object(
53+
'organization',
54+
NEW.id
55+
)
56+
)
57+
)::text,
58+
'application/json'
59+
);
60+
END IF;
61+
EXCEPTION
62+
WHEN OTHERS THEN NULL;
63+
END;
64+
END IF;
65+
END IF;
66+
RETURN NEW;
67+
END;
68+
$$;
69+
CREATE TRIGGER track_organization_onboarding_trigger
70+
AFTER
71+
UPDATE ON public.organization FOR EACH ROW EXECUTE FUNCTION public.track_organization_onboarding();

web/lib/clients/posthogClient.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
export class PosthogClient {
2+
private static instance: PosthogClient;
3+
private isEnabled: boolean;
4+
private apiKey: string | null;
5+
6+
private constructor() {
7+
this.apiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY || null;
8+
this.isEnabled =
9+
!!this.apiKey &&
10+
process.env.NEXT_PUBLIC_DISABLE_POSTHOG !== "true" &&
11+
process.env.NODE_ENV !== "development";
12+
}
13+
14+
static getInstance(): PosthogClient {
15+
if (!PosthogClient.instance) {
16+
PosthogClient.instance = new PosthogClient();
17+
}
18+
return PosthogClient.instance;
19+
}
20+
21+
public async captureEvent(
22+
eventName: string,
23+
properties: Record<string, any> = {},
24+
userId?: string,
25+
organizationId?: string
26+
): Promise<boolean> {
27+
if (!this.isEnabled || !this.apiKey) {
28+
console.log(`[PostHog Disabled] Would have sent: ${eventName}`);
29+
return false;
30+
}
31+
32+
try {
33+
const payload = {
34+
api_key: this.apiKey,
35+
event: eventName,
36+
distinct_id: userId || "server",
37+
properties: {
38+
...properties,
39+
...(organizationId
40+
? { $groups: { organization: organizationId } }
41+
: {}),
42+
},
43+
};
44+
45+
const response = await fetch("https://app.posthog.com/capture/", {
46+
method: "POST",
47+
headers: { "Content-Type": "application/json" },
48+
body: JSON.stringify(payload),
49+
});
50+
51+
const success = response.status === 200;
52+
if (!success) {
53+
console.error(
54+
`PostHog Error (${response.status}): ${await response.text()}`
55+
);
56+
}
57+
return success;
58+
} catch (error) {
59+
console.error("PostHog Capture Error:", error);
60+
return false;
61+
}
62+
}
63+
}

web/pages/api/stripe/event_webhook/index.ts

+4-44
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { buildDynamicUpdateQuery, dbExecute } from "@/lib/api/db/dbExecute";
21
import {
32
getEvaluatorUsage,
43
getExperimentUsage,
@@ -7,6 +6,8 @@ import { getHeliconeAuthClient } from "@/packages/common/auth/server/AuthClientF
76
import { costOf } from "@/packages/cost";
87
import { OnboardingState } from "@/services/hooks/useOrgOnboarding";
98
import { WebClient } from "@slack/web-api";
9+
import { buildDynamicUpdateQuery, dbExecute } from "@/lib/api/db/dbExecute";
10+
import { PosthogClient } from "@/lib/clients/posthogClient";
1011
import generateApiKey from "generate-api-key";
1112
import { buffer } from "micro";
1213
import { NextApiRequest, NextApiResponse } from "next";
@@ -37,47 +38,6 @@ async function getUserIdFromEmail(email: string): Promise<string | null> {
3738
}
3839
}
3940

40-
async function sendPosthogEvent(
41-
event: string,
42-
properties: Record<string, any>,
43-
userId: string,
44-
orgId?: string
45-
) {
46-
try {
47-
if (!userId) {
48-
console.error(
49-
`Cannot send PostHog event: missing userId for event ${event}`
50-
);
51-
return;
52-
}
53-
54-
const posthogPayload = {
55-
api_key: process.env.NEXT_PUBLIC_POSTHOG_API_KEY,
56-
event: event,
57-
distinct_id: userId,
58-
properties: {
59-
...properties,
60-
...(orgId ? { $groups: { organization: orgId } } : {}),
61-
},
62-
};
63-
64-
const response = await fetch(POSTHOG_EVENT_API, {
65-
method: "POST",
66-
headers: { "Content-Type": "application/json" },
67-
body: JSON.stringify(posthogPayload),
68-
});
69-
70-
console.log(`PostHog: Event response for ${event}: ${response.status}`);
71-
72-
if (!response.ok) {
73-
const responseText = await response.text();
74-
console.error(`PostHog error: ${responseText}`);
75-
}
76-
} catch (error) {
77-
console.error(`Error sending event to PostHog:`, error);
78-
}
79-
}
80-
8141
const ADDON_PRICES: Record<string, keyof Addons> = {
8242
[process.env.PRICE_PROD_ALERTS_ID!]: "alerts",
8343
[process.env.PRICE_PROD_PROMPTS_ID!]: "prompts",
@@ -165,8 +125,8 @@ async function sendSubscriptionEvent(
165125
: "immediate";
166126
}
167127

168-
// Send the event
169-
await sendPosthogEvent(
128+
const analytics = PosthogClient.getInstance();
129+
await analytics.captureEvent(
170130
eventType,
171131
{
172132
...baseProperties,

web/pages/signup.tsx

-11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import AuthForm from "../components/templates/auth/authForm";
88
import { DEMO_EMAIL } from "../lib/constants";
99
import PublicMetaData from "../components/layout/public/publicMetaData";
1010
import { GetServerSidePropsContext } from "next";
11-
import posthog from "posthog-js";
1211
import { InfoBanner } from "../components/shared/themed/themedDemoBanner";
1312
import { env } from "next-runtime-env";
1413
const SignUp = () => {
@@ -42,10 +41,6 @@ const SignUp = () => {
4241
<AuthForm
4342
handleEmailSubmit={async (email: string, password: string) => {
4443
const origin = window.location.origin;
45-
posthog.capture("user_signed_up", {
46-
method: "email",
47-
email: email,
48-
});
4944

5045
const { data, error } = await supabase.auth.signUp({
5146
email: email,
@@ -68,9 +63,6 @@ const SignUp = () => {
6863
setShowEmailConfirmation(true);
6964
}}
7065
handleGoogleSubmit={async () => {
71-
posthog.capture("user_signed_up", {
72-
method: "google",
73-
});
7466
const { error } = await supabase.auth.signInWithOAuth({
7567
provider: "google",
7668
options: {
@@ -87,9 +79,6 @@ const SignUp = () => {
8779
}
8880
}}
8981
handleGithubSubmit={async () => {
90-
posthog.capture("user_signed_up", {
91-
method: "github",
92-
});
9382
const { error } = await supabase.auth.signInWithOAuth({
9483
provider: "github",
9584
options: {

0 commit comments

Comments
 (0)