Skip to content

Commit a90586b

Browse files
committed
analytics
1 parent 283ec5c commit a90586b

10 files changed

Lines changed: 242 additions & 1 deletion

File tree

frontend/bun.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"class-variance-authority": "^0.7.1",
1919
"clsx": "^2.1.1",
2020
"lucide-react": "^0.562.0",
21+
"posthog-js": "^1.313.0",
2122
"react": "^19",
2223
"react-dom": "^19",
2324
"recharts": "2.15.4",

frontend/src/bundle.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@
7373
});
7474

7575
this.instances.push(instance);
76+
77+
// Track widget initialization
78+
trackWidget('widget_initialized', {
79+
domain,
80+
page_id: pageId,
81+
theme,
82+
is_localhost: isLocalhost()
83+
});
84+
7685
return instance;
7786
},
7887
};

frontend/src/components/docs-page.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Zap
1515
} from "lucide-react";
1616
import { cn } from "../lib/utils";
17+
import { trackEvent } from "../lib/analytics";
1718

1819
// --- Components ---
1920

@@ -24,6 +25,11 @@ function CodeBlock({ code, language = "html" }: { code: string; language?: strin
2425
navigator.clipboard.writeText(code);
2526
setCopied(true);
2627
setTimeout(() => setCopied(false), 2000);
28+
// Track code copy
29+
trackEvent('docs_code_copied', {
30+
language,
31+
code_length: code.length
32+
});
2733
};
2834

2935
return (
@@ -92,7 +98,12 @@ function FrameworkTabs() {
9298
return (
9399
<button
94100
key={tab.id}
95-
onClick={() => setActive(tab.id as any)}
101+
onClick={() => {
102+
setActive(tab.id as any);
103+
trackEvent('docs_framework_tab_clicked', {
104+
framework: tab.id
105+
});
106+
}}
96107
className={cn(
97108
"flex items-center gap-2 px-6 py-4 text-sm font-medium border-r border-border transition-all hover:bg-muted/50",
98109
active === tab.id
@@ -207,9 +218,20 @@ export function DocsPage() {
207218
element.scrollIntoView({ behavior: "smooth" });
208219
setActiveSection(id);
209220
setMobileMenuOpen(false);
221+
// Track section navigation
222+
trackEvent('docs_section_viewed', {
223+
section_id: id
224+
});
210225
}
211226
};
212227

228+
// Track initial page view
229+
React.useEffect(() => {
230+
trackEvent('docs_page_viewed', {
231+
initial_section: 'introduction'
232+
});
233+
}, []);
234+
213235
return (
214236
<div className="min-h-screen bg-background text-foreground flex flex-col md:flex-row">
215237
{/* Mobile Header */}

frontend/src/components/login-form.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from 'react';
22
import { useAuth } from '@/lib/auth-context';
3+
import { trackEvent, Events } from '@/lib/analytics';
34
import { Button } from '@/components/ui/button';
45
import { Input } from '@/components/ui/input';
56
import { Label } from '@/components/ui/label';
@@ -62,6 +63,11 @@ export function LoginForm() {
6263

6364
if (result.success) {
6465
setSent(true);
66+
// Track successful magic link request
67+
trackEvent(Events.USER_LOGGED_IN, {
68+
method: 'magic_link',
69+
email_domain: email.split('@')[1]
70+
});
6571
} else {
6672
setError(result.error || 'Failed to send magic link');
6773
}

frontend/src/docs.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<link rel="icon" type="image/png" href="/icon.png" />
88
<title>CommentKit - Documentation</title>
9+
<script>
10+
!function (t, e) { var o, n, p, r; e.__SV || (window.posthog && window.posthog.__loaded) || (window.posthog = e, e._i = [], e.init = function (i, s, a) { function g(t, e) { var o = e.split("."); 2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () { t.push([e].concat(Array.prototype.slice.call(arguments, 0))) } } (p = t.createElement("script")).type = "text/javascript", p.crossOrigin = "anonymous", p.async = !0, p.src = s.api_host.replace(".i.posthog.com", "-assets.i.posthog.com") + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r); var u = e; for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) { var e = "posthog"; return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e }, u.people.toString = function () { return u.toString(1) + ".people (stub)" }, o = "init Xr es pi Zr rs Kr Qr capture Ni calculateEventProperties os register register_once register_for_session unregister unregister_for_session ds getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException startExceptionAutocapture stopExceptionAutocapture loadToolbar get_property getSessionProperty us ns createPersonProfile hs Vr vs opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing ss debug O ls getPageViewId captureTraceFeedback captureTraceMetric qr".split(" "), n = 0; n < o.length; n++)g(u, o[n]); e._i.push([i, s, a]) }, e.__SV = 1) }(document, window.posthog || []);
11+
posthog.init('phc_nBvIMuMmaqQkAXL36Bi6eyTRga0A04klevYFyZf4cfc', {
12+
api_host: 'https://eu.i.posthog.com',
13+
person_profiles: 'identified_only'
14+
})
15+
</script>
916
<script type="module" src="./docs.tsx" async></script>
1017
</head>
1118

frontend/src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<link rel="icon" type="image/png" href="/icon.png" />
88
<title>CommentKit</title>
9+
<script>
10+
!function (t, e) { var o, n, p, r; e.__SV || (window.posthog && window.posthog.__loaded) || (window.posthog = e, e._i = [], e.init = function (i, s, a) { function g(t, e) { var o = e.split("."); 2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () { t.push([e].concat(Array.prototype.slice.call(arguments, 0))) } } (p = t.createElement("script")).type = "text/javascript", p.crossOrigin = "anonymous", p.async = !0, p.src = s.api_host.replace(".i.posthog.com", "-assets.i.posthog.com") + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r); var u = e; for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) { var e = "posthog"; return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e }, u.people.toString = function () { return u.toString(1) + ".people (stub)" }, o = "init Xr es pi Zr rs Kr Qr capture Ni calculateEventProperties os register register_once register_for_session unregister unregister_for_session ds getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException startExceptionAutocapture stopExceptionAutocapture loadToolbar get_property getSessionProperty us ns createPersonProfile hs Vr vs opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing ss debug O ls getPageViewId captureTraceFeedback captureTraceMetric qr".split(" "), n = 0; n < o.length; n++)g(u, o[n]); e._i.push([i, s, a]) }, e.__SV = 1) }(document, window.posthog || []);
11+
posthog.init('phc_nBvIMuMmaqQkAXL36Bi6eyTRga0A04klevYFyZf4cfc', {
12+
api_host: 'https://eu.i.posthog.com',
13+
person_profiles: 'identified_only'
14+
})
15+
</script>
916
<script type="module" src="./frontend.tsx" async></script>
1017
</head>
1118

frontend/src/lib/analytics.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// PostHog Analytics Utility
2+
// This provides type-safe wrapper functions for PostHog analytics
3+
4+
declare global {
5+
interface Window {
6+
posthog: any;
7+
}
8+
}
9+
10+
/**
11+
* Track a custom event in PostHog
12+
* @param eventName - Name of the event to track
13+
* @param properties - Optional properties to attach to the event
14+
*/
15+
export const trackEvent = (eventName: string, properties?: Record<string, any>) => {
16+
if (typeof window !== 'undefined' && window.posthog) {
17+
window.posthog.capture(eventName, properties);
18+
}
19+
};
20+
21+
/**
22+
* Identify a user in PostHog
23+
* @param userId - Unique identifier for the user
24+
* @param properties - Optional user properties
25+
*/
26+
export const identifyUser = (userId: string, properties?: Record<string, any>) => {
27+
if (typeof window !== 'undefined' && window.posthog) {
28+
window.posthog.identify(userId, properties);
29+
}
30+
};
31+
32+
/**
33+
* Reset the user identity (e.g., on logout)
34+
*/
35+
export const resetUser = () => {
36+
if (typeof window !== 'undefined' && window.posthog) {
37+
window.posthog.reset();
38+
}
39+
};
40+
41+
/**
42+
* Set user properties
43+
* @param properties - User properties to set
44+
*/
45+
export const setUserProperties = (properties: Record<string, any>) => {
46+
if (typeof window !== 'undefined' && window.posthog) {
47+
window.posthog.setPersonProperties(properties);
48+
}
49+
};
50+
51+
/**
52+
* Check if a feature flag is enabled
53+
* @param flagKey - The feature flag key
54+
* @returns boolean indicating if the flag is enabled
55+
*/
56+
export const isFeatureEnabled = (flagKey: string): boolean => {
57+
if (typeof window !== 'undefined' && window.posthog) {
58+
return window.posthog.isFeatureEnabled(flagKey);
59+
}
60+
return false;
61+
};
62+
63+
/**
64+
* Get feature flag value
65+
* @param flagKey - The feature flag key
66+
* @returns The feature flag value
67+
*/
68+
export const getFeatureFlag = (flagKey: string): any => {
69+
if (typeof window !== 'undefined' && window.posthog) {
70+
return window.posthog.getFeatureFlag(flagKey);
71+
}
72+
return null;
73+
};
74+
75+
// Common event names for consistency
76+
export const Events = {
77+
// Authentication
78+
USER_LOGGED_IN: 'user_logged_in',
79+
USER_LOGGED_OUT: 'user_logged_out',
80+
USER_REGISTERED: 'user_registered',
81+
82+
// Comments
83+
COMMENT_CREATED: 'comment_created',
84+
COMMENT_DELETED: 'comment_deleted',
85+
COMMENT_EDITED: 'comment_edited',
86+
COMMENT_REPLIED: 'comment_replied',
87+
88+
// Page views
89+
PAGE_VIEWED: 'page_viewed',
90+
DASHBOARD_VIEWED: 'dashboard_viewed',
91+
92+
// Interactions
93+
BUTTON_CLICKED: 'button_clicked',
94+
FORM_SUBMITTED: 'form_submitted',
95+
MODAL_OPENED: 'modal_opened',
96+
MODAL_CLOSED: 'modal_closed',
97+
} as const;

frontend/src/lib/auth-context.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext, useContext, useEffect, useState, useRef, type ReactNode } from 'react';
22
import { auth, type User, type BootstrapData, setCsrfToken } from './api';
3+
import { identifyUser, setUserProperties, trackEvent, Events, resetUser } from './analytics';
34

45
interface AuthContextType {
56
user: User | null;
@@ -40,6 +41,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
4041
// Set user without bootstrap property
4142
const { bootstrap: _, ...userData } = data;
4243
setUser(userData);
44+
// Track user in analytics
45+
if (userData.id && userData.email) {
46+
identifyUser(userData.id.toString(), {
47+
email: userData.email,
48+
created_at: userData.created_at
49+
});
50+
}
4351
}
4452
setLoading(false);
4553
};
@@ -61,6 +69,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
6169
// No need to store token - server sets HttpOnly cookie
6270
setUser(data.user);
6371

72+
// Track successful authentication
73+
if (data.user.id && data.user.email) {
74+
identifyUser(data.user.id.toString(), {
75+
email: data.user.email,
76+
created_at: data.user.created_at
77+
});
78+
trackEvent(Events.USER_LOGGED_IN, {
79+
method: 'magic_link_verified',
80+
redirect_url: redirectUrl || null
81+
});
82+
}
83+
6484
// Redirect to the original page if specified
6585
if (redirectUrl) {
6686
window.location.href = redirectUrl;
@@ -86,11 +106,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
86106
};
87107

88108
const logout = async () => {
109+
// Track logout before clearing user data
110+
trackEvent(Events.USER_LOGGED_OUT);
89111
// Server will clear HttpOnly cookie
90112
await auth.logout();
91113
setUser(null);
92114
setCsrfToken(null);
93115
bootstrapRef.current = null;
116+
// Reset analytics user
117+
resetUser();
94118
};
95119

96120
const updateUser = (updatedUser: User) => {

0 commit comments

Comments
 (0)