(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1export type AnalyticsEvents = {
2 login_initiated: { handle: string };
3 login_success: { handle: string; pds?: string };
4 signup_initiated: { provider: string };
5 user_logged_out: Record<string, never>;
6
7 annotation_created: {
8 url: string;
9 has_quote: boolean;
10 tag_count: number;
11 has_labels: boolean;
12 source?: "web" | "extension";
13 };
14 highlight_created: {
15 url: string;
16 tag_count: number;
17 has_color: boolean;
18 has_labels: boolean;
19 source?: "web" | "extension";
20 };
21 bookmark_created: {
22 url: string;
23 tag_count: number;
24 source?: "web" | "extension";
25 };
26 reply_created: { parent_uri: string; root_uri: string };
27
28 item_liked: {
29 action: "like" | "unlike";
30 type: "annotation" | "highlight" | "bookmark";
31 };
32 item_deleted: { type: "annotation" | "highlight" | "bookmark" };
33 item_shared: {
34 method: "copy" | "bluesky" | "twitter" | "mastodon" | "email";
35 item_type?: string;
36 };
37 item_added_to_collection: Record<string, never>;
38
39 collection_created: { name: string };
40 collection_deleted: Record<string, never>;
41
42 extension_installed: { version: string; browser: string };
43 extension_connected: { did: string };
44 popup_opened: { authenticated: boolean };
45 extension_tab_switched: { tab: string };
46
47 highlights_imported: { total: number; completed: number; failed: number };
48
49 search_performed: { query: string };
50
51 api_key_created: Record<string, never>;
52 theme_changed: { theme: string };
53};
54
55function getPostHog() {
56 if (typeof window === "undefined") return null;
57 return window.posthog ?? null;
58}
59
60export const analytics = {
61 capture<E extends keyof AnalyticsEvents>(
62 event: E,
63 properties?: AnalyticsEvents[E],
64 ): void {
65 try {
66 getPostHog()?.capture(
67 event as string,
68 properties as Record<string, unknown>,
69 );
70 } catch {
71 // ignore
72 }
73 },
74
75 identify(
76 did: string,
77 properties: { handle: string; displayName?: string },
78 ): void {
79 try {
80 getPostHog()?.identify(did, {
81 handle: properties.handle,
82 display_name: properties.displayName ?? undefined,
83 $set_once: { first_seen_at: new Date().toISOString() },
84 });
85 } catch {
86 // noop
87 }
88 },
89
90 reset(): void {
91 try {
92 getPostHog()?.reset();
93 } catch {
94 // noop
95 }
96 },
97
98 captureException(error: unknown, properties?: Record<string, unknown>): void {
99 try {
100 const ph = getPostHog();
101 if (!ph) return;
102 if (typeof ph.captureException === "function") {
103 ph.captureException(error, properties);
104 } else {
105 ph.capture("$exception", {
106 message: error instanceof Error ? error.message : String(error),
107 ...properties,
108 });
109 }
110 } catch {
111 // noop
112 }
113 },
114};