From 46a3c1d6d4986eb2dffc1925d93e54f1a177419b Mon Sep 17 00:00:00 2001 From: Edwin Tantawi Date: Sun, 25 Jan 2026 14:51:35 +0700 Subject: [PATCH 1/4] feat(analytic): add posthog wrapper for analytic capture --- src/lib/analytic/events.ts | 19 +++++++++++++++++++ src/lib/analytic/index.ts | 25 +++++++++++++++++++++++++ src/lib/analytic/properties.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/lib/analytic/events.ts create mode 100644 src/lib/analytic/index.ts create mode 100644 src/lib/analytic/properties.ts diff --git a/src/lib/analytic/events.ts b/src/lib/analytic/events.ts new file mode 100644 index 0000000..5d6ca78 --- /dev/null +++ b/src/lib/analytic/events.ts @@ -0,0 +1,19 @@ +/** + * Analytics events + * + * Uses the `category:object_action` naming convention to ensure consistent and descriptive event naming. + * - **category**: The context or domain where the event occurred (e.g., `auth`, `account_settings`, `signup_flow`) + * - **object**: The component, feature, or location involved (e.g., `signup_button`, `pricing_page`) + * - **action**: The user action or system event that occurred (e.g., `click`, `submit`, `view`, `cancel`) + * + * @examples + * - `account_settings:forgot_password_button_click` + * - `signup_flow:pricing_page_view` + * - `registration:sign_up_button_click` + * - `registration_v2:sign_up_button_click` - version your events + * + * @see {@link https://posthog.com/docs/product-analytics/best-practices#2-implement-a-naming-convention} + */ +export enum AnalyticEvent { + EXAMPLE = 'example:view_example', +} diff --git a/src/lib/analytic/index.ts b/src/lib/analytic/index.ts new file mode 100644 index 0000000..26b3a33 --- /dev/null +++ b/src/lib/analytic/index.ts @@ -0,0 +1,25 @@ +import { usePostHog } from '@posthog/react'; +import type { CaptureOptions } from 'posthog-js'; + +import { AnalyticEvent } from '~/lib/analytic/events'; +import type { AnalyticProperty } from '~/lib/analytic/properties'; + +/** + * Hook for capturing analytics events using PostHog. + * + * Provides a typed interface for tracking analytics events with associated properties. + * This hook wraps the PostHog client to ensure type-safe event capturing. + */ +export function useAnalytic() { + const posthog = usePostHog(); + + function capture( + event: TEvent, + properties?: AnalyticProperty[TEvent] | null, + options?: CaptureOptions, + ) { + posthog.capture(event, properties, options); + } + + return { ...posthog, capture }; +} diff --git a/src/lib/analytic/properties.ts b/src/lib/analytic/properties.ts new file mode 100644 index 0000000..9a43744 --- /dev/null +++ b/src/lib/analytic/properties.ts @@ -0,0 +1,27 @@ +import type { AnalyticEvent } from '~/lib/analytic/events'; + +/** + * Analytics properties - contextual data attached to events + * + * Uses naming conventions to ensure consistent and descriptive property naming. + * - **object_adjective pattern**: Use descriptive property names (e.g., `user_id`, `item_price`, `member_count`) + * - **boolean prefixes**: Use `is_` or `has_` for boolean properties (e.g., `is_subscribed`, `has_seen_upsell`) + * - **temporal suffixes**: For dates/timestamps, include `_date` or `_timestamp` (e.g., `user_creation_date`, `last_login_timestamp`) + * + * @examples + * - `user_id` + * - `item_price` + * - `member_count` + * - `is_subscribed` + * - `has_seen_upsell` + * - `last_login_timestamp` + * - `user_creation_date` + * + * @see {@link https://posthog.com/docs/product-analytics/best-practices#2-implement-a-naming-convention} + */ +export interface AnalyticProperty { + [AnalyticEvent.EXAMPLE]: { + example_id: string; + example_timestamp: number; + }; +} From 16ee09066fd8cc5de729ae180f08b6ca8a9c6fbf Mon Sep 17 00:00:00 2001 From: Edwin Tantawi Date: Sun, 25 Jan 2026 15:17:53 +0700 Subject: [PATCH 2/4] feat(analytic): add server analytic with posthog-node --- src/lib/analytic/index.ts | 37 +++++++++++++++++++++++++++++++++++-- src/lib/analytic/types.ts | 9 +++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/lib/analytic/types.ts diff --git a/src/lib/analytic/index.ts b/src/lib/analytic/index.ts index 26b3a33..7174bc5 100644 --- a/src/lib/analytic/index.ts +++ b/src/lib/analytic/index.ts @@ -3,6 +3,8 @@ import type { CaptureOptions } from 'posthog-js'; import { AnalyticEvent } from '~/lib/analytic/events'; import type { AnalyticProperty } from '~/lib/analytic/properties'; +import type { AnalyticEventMessage } from '~/lib/analytic/types'; +import { createPosthogClient } from '~/lib/posthog/server'; /** * Hook for capturing analytics events using PostHog. @@ -15,11 +17,42 @@ export function useAnalytic() { function capture( event: TEvent, - properties?: AnalyticProperty[TEvent] | null, + properties: AnalyticProperty[TEvent], options?: CaptureOptions, ) { - posthog.capture(event, properties, options); + return posthog.capture(event, properties, options); } return { ...posthog, capture }; } + +/** + * Creates a server-side analytics client using PostHog. + * + * This function initializes a PostHog client for server-side analytics tracking, + * providing a type-safe interface for capturing analytics events immediately. + * Useful for tracking events that occur during server-side operations. + */ +export function createAnalyticClient() { + const posthog = createPosthogClient(); + + function captureImmediate( + props: AnalyticEventMessage, + ) { + return posthog.captureImmediate({ + event: props.event, + properties: props.properties || undefined, + }); + } + + function capture( + props: AnalyticEventMessage, + ) { + return posthog.capture({ + event: props.event, + properties: props.properties || undefined, + }); + } + + return { ...posthog, capture, captureImmediate }; +} diff --git a/src/lib/analytic/types.ts b/src/lib/analytic/types.ts new file mode 100644 index 0000000..0fc5f68 --- /dev/null +++ b/src/lib/analytic/types.ts @@ -0,0 +1,9 @@ +import type { EventMessage } from 'posthog-node'; + +import type { AnalyticEvent } from '~/lib/analytic/events'; +import type { AnalyticProperty } from '~/lib/analytic/properties'; + +export type AnalyticEventMessage = { + event: TEvent; + properties: AnalyticProperty[TEvent]; +} & Omit; From 82cfb54bb5207234a281f5cb0f243c04ffdb09e2 Mon Sep 17 00:00:00 2001 From: Edwin Tantawi Date: Mon, 26 Jan 2026 08:00:16 +0700 Subject: [PATCH 3/4] feat: remove example event and property --- src/lib/analytic/events.ts | 4 +--- src/lib/analytic/index.ts | 10 +++++----- src/lib/analytic/properties.ts | 9 +-------- src/lib/analytic/types.ts | 4 +++- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/lib/analytic/events.ts b/src/lib/analytic/events.ts index 5d6ca78..e70c780 100644 --- a/src/lib/analytic/events.ts +++ b/src/lib/analytic/events.ts @@ -14,6 +14,4 @@ * * @see {@link https://posthog.com/docs/product-analytics/best-practices#2-implement-a-naming-convention} */ -export enum AnalyticEvent { - EXAMPLE = 'example:view_example', -} +export enum AnalyticEvent {} diff --git a/src/lib/analytic/index.ts b/src/lib/analytic/index.ts index 7174bc5..8cad68a 100644 --- a/src/lib/analytic/index.ts +++ b/src/lib/analytic/index.ts @@ -15,7 +15,7 @@ import { createPosthogClient } from '~/lib/posthog/server'; export function useAnalytic() { const posthog = usePostHog(); - function capture( + function capture( event: TEvent, properties: AnalyticProperty[TEvent], options?: CaptureOptions, @@ -36,16 +36,16 @@ export function useAnalytic() { export function createAnalyticClient() { const posthog = createPosthogClient(); - function captureImmediate( - props: AnalyticEventMessage, - ) { + function captureImmediate< + TEvent extends AnalyticEvent & keyof AnalyticProperty, + >(props: AnalyticEventMessage) { return posthog.captureImmediate({ event: props.event, properties: props.properties || undefined, }); } - function capture( + function capture( props: AnalyticEventMessage, ) { return posthog.capture({ diff --git a/src/lib/analytic/properties.ts b/src/lib/analytic/properties.ts index 9a43744..940abd9 100644 --- a/src/lib/analytic/properties.ts +++ b/src/lib/analytic/properties.ts @@ -1,5 +1,3 @@ -import type { AnalyticEvent } from '~/lib/analytic/events'; - /** * Analytics properties - contextual data attached to events * @@ -19,9 +17,4 @@ import type { AnalyticEvent } from '~/lib/analytic/events'; * * @see {@link https://posthog.com/docs/product-analytics/best-practices#2-implement-a-naming-convention} */ -export interface AnalyticProperty { - [AnalyticEvent.EXAMPLE]: { - example_id: string; - example_timestamp: number; - }; -} +export interface AnalyticProperty {} diff --git a/src/lib/analytic/types.ts b/src/lib/analytic/types.ts index 0fc5f68..ec0996e 100644 --- a/src/lib/analytic/types.ts +++ b/src/lib/analytic/types.ts @@ -3,7 +3,9 @@ import type { EventMessage } from 'posthog-node'; import type { AnalyticEvent } from '~/lib/analytic/events'; import type { AnalyticProperty } from '~/lib/analytic/properties'; -export type AnalyticEventMessage = { +export type AnalyticEventMessage< + TEvent extends AnalyticEvent & keyof AnalyticProperty, +> = { event: TEvent; properties: AnalyticProperty[TEvent]; } & Omit; From 1a1a363f2f60c60acecd1ec74c29549565ad16b9 Mon Sep 17 00:00:00 2001 From: Edwin Tantawi Date: Sun, 12 Apr 2026 19:56:27 +0700 Subject: [PATCH 4/4] refactor(analytic): simplify capture API with conditional property types --- src/lib/analytic/events.ts | 4 ++- src/lib/analytic/index.ts | 61 +++++++++++++++++++--------------- src/lib/analytic/properties.ts | 8 ++++- src/lib/analytic/types.ts | 24 ++++++++----- 4 files changed, 60 insertions(+), 37 deletions(-) diff --git a/src/lib/analytic/events.ts b/src/lib/analytic/events.ts index e70c780..ec5643b 100644 --- a/src/lib/analytic/events.ts +++ b/src/lib/analytic/events.ts @@ -14,4 +14,6 @@ * * @see {@link https://posthog.com/docs/product-analytics/best-practices#2-implement-a-naming-convention} */ -export enum AnalyticEvent {} +export enum AnalyticEvent { + EVENT = 'category:object_action' /** Example */, +} diff --git a/src/lib/analytic/index.ts b/src/lib/analytic/index.ts index 8cad68a..d86f4fd 100644 --- a/src/lib/analytic/index.ts +++ b/src/lib/analytic/index.ts @@ -1,9 +1,10 @@ import { usePostHog } from '@posthog/react'; import type { CaptureOptions } from 'posthog-js'; +import type { EventMessage } from 'posthog-node'; +import * as React from 'react'; -import { AnalyticEvent } from '~/lib/analytic/events'; -import type { AnalyticProperty } from '~/lib/analytic/properties'; -import type { AnalyticEventMessage } from '~/lib/analytic/types'; +import { type AnalyticEvent } from '~/lib/analytic/events'; +import type { AnalyticCaptureEventProps } from '~/lib/analytic/types'; import { createPosthogClient } from '~/lib/posthog/server'; /** @@ -15,15 +16,15 @@ import { createPosthogClient } from '~/lib/posthog/server'; export function useAnalytic() { const posthog = usePostHog(); - function capture( - event: TEvent, - properties: AnalyticProperty[TEvent], - options?: CaptureOptions, - ) { - return posthog.capture(event, properties, options); - } - - return { ...posthog, capture }; + return React.useRef({ + capture({ + event, + properties, + ...options + }: AnalyticCaptureEventProps) { + return posthog.capture(event, properties, options); + }, + }).current; } /** @@ -36,23 +37,29 @@ export function useAnalytic() { export function createAnalyticClient() { const posthog = createPosthogClient(); - function captureImmediate< - TEvent extends AnalyticEvent & keyof AnalyticProperty, - >(props: AnalyticEventMessage) { - return posthog.captureImmediate({ - event: props.event, - properties: props.properties || undefined, - }); + /** Capture an event immediately (synchronously) */ + function captureImmediate({ + event, + properties, + ...props + }: AnalyticCaptureEventProps< + TEvent, + Omit + >) { + return posthog.captureImmediate({ event, properties, ...props }); } - function capture( - props: AnalyticEventMessage, - ) { - return posthog.capture({ - event: props.event, - properties: props.properties || undefined, - }); + /** Capture an event manually (asynchronously) */ + function capture({ + event, + properties, + ...props + }: AnalyticCaptureEventProps< + TEvent, + Omit + >) { + return posthog.capture({ event, properties, ...props }); } - return { ...posthog, capture, captureImmediate }; + return { capture, captureImmediate }; } diff --git a/src/lib/analytic/properties.ts b/src/lib/analytic/properties.ts index 940abd9..332a818 100644 --- a/src/lib/analytic/properties.ts +++ b/src/lib/analytic/properties.ts @@ -1,3 +1,5 @@ +import type { AnalyticEvent } from '~/lib/analytic/events'; + /** * Analytics properties - contextual data attached to events * @@ -17,4 +19,8 @@ * * @see {@link https://posthog.com/docs/product-analytics/best-practices#2-implement-a-naming-convention} */ -export interface AnalyticProperty {} +export interface AnalyticProperty { + [AnalyticEvent.EVENT]: { + event_id: string; + } /** Example */; +} diff --git a/src/lib/analytic/types.ts b/src/lib/analytic/types.ts index ec0996e..61a8475 100644 --- a/src/lib/analytic/types.ts +++ b/src/lib/analytic/types.ts @@ -1,11 +1,19 @@ -import type { EventMessage } from 'posthog-node'; - import type { AnalyticEvent } from '~/lib/analytic/events'; import type { AnalyticProperty } from '~/lib/analytic/properties'; -export type AnalyticEventMessage< - TEvent extends AnalyticEvent & keyof AnalyticProperty, -> = { - event: TEvent; - properties: AnalyticProperty[TEvent]; -} & Omit; +/** + * Props for capturing an analytics event. + * + * Conditionally requires `properties` based on whether the event has a + * corresponding entry in `AnalyticProperty`. If the event is mapped, `properties` + * is required and typed accordingly; otherwise `properties` is not allowed. + * + * @template TEvent - The analytic event type being captured. + * @template TOthers - Additional options to merge into the props (e.g. PostHog `CaptureOptions`). + */ +export type AnalyticCaptureEventProps< + TEvent extends AnalyticEvent, + TOthers extends {} = {}, +> = TEvent extends keyof AnalyticProperty + ? { event: TEvent; properties: AnalyticProperty[TEvent] } & TOthers + : { event: TEvent; properties?: undefined } & TOthers;