diff --git a/packages/browser/package.json b/packages/browser/package.json index 15de478253..8dff31a47f 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.275.1", + "version": "1.275.2", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", diff --git a/packages/browser/playground/nextjs/src/README.md b/packages/browser/playground/nextjs/src/README.md new file mode 100644 index 0000000000..46987f8522 --- /dev/null +++ b/packages/browser/playground/nextjs/src/README.md @@ -0,0 +1,39 @@ +# PostHog Typed Events - Generated File Example + +## Overview + +This directory contains an example of a generated typed PostHog wrapper that provides type-safe event tracking. + +## File: `posthog-typed.ts` + +This file demonstrates what the PostHog CLI would generate to provide type safety for event tracking. + +### Features + +- **`captureTyped()`** - Type-safe event capture with compile-time validation +- **`captureUntyped()`** - Flexible capture for dynamic/untyped events +- Enforces required properties and types +- Allows additional properties beyond the schema +- Works with all event naming patterns (spaces, hyphens, single words, etc.) +- Backward compatible with deprecated `capture()` method + +### Usage + +```typescript +import posthog from './posthog-typed' + +// Type-safe capture - TypeScript enforces all required properties +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42, + quantity: 1 +}) + +// Flexible capture for dynamic events +posthog.captureUntyped('Custom Event', { any: 'data' }) +``` + +## Test File + +See `../../../test-captureTyped-simple.ts` for examples of type checking in action. diff --git a/packages/browser/playground/nextjs/src/posthog-typed.ts b/packages/browser/playground/nextjs/src/posthog-typed.ts new file mode 100644 index 0000000000..c861eb2b46 --- /dev/null +++ b/packages/browser/playground/nextjs/src/posthog-typed.ts @@ -0,0 +1,163 @@ +/** + * GENERATED FILE - DO NOT EDIT + * + * This file was auto-generated by PostHog CLI + * + * Provides captureTyped() for type-safe events and captureUntyped() for flexibility + */ + +import originalPostHog from 'posthog-js' +import type { PostHog as OriginalPostHog, CaptureOptions, CaptureResult, Properties } from 'posthog-js' + +// Product type used in multiple events +interface Product { + product_id: string + name: string + price: number +} + +// Define event schemas +interface EventSchemas { + 'Product Added': { + product_id: string + name: string + price: number + quantity: number + } + 'Products Searched': { + products: Product[] + } + 'Product Added to Wishlist': { + product_id: string + name: string + price: number + quantity: number + wishlist_id: string + wishlist_name: string + } + 'Order Completed': { + products: Product[] + total: number + currency: string + } + 'Custom Event': { + foo: string + } + 'Logged in': Record + 'Logged out': Record + 'Clicked button': Record +} + +// Enhanced PostHog interface with both typed and untyped capture +interface TypedPostHog extends Omit { + /** + * Type-safe capture for defined events + * + * Note: Additional properties beyond the schema are allowed + * + * @example + * posthog.captureTyped('Product Added', { + * product_id: '123', + * name: 'Widget', + * price: 42, + * quantity: 1, + * custom_field: 'extra' // additional properties allowed + * }) + */ + captureTyped( + event_name: K, + properties: P & Record, + options?: CaptureOptions + ): CaptureResult | undefined + + /** + * Flexible capture for any event (original behavior) + * + * @example + * posthog.captureUntyped('Custom Event Name', { any: 'data' }) + */ + captureUntyped( + event_name: string, + properties?: Properties | null, + options?: CaptureOptions + ): CaptureResult | undefined + + /** + * @deprecated Use captureTyped() for type safety or captureUntyped() for flexibility + */ + capture(event_name: string, properties?: Properties | null, options?: CaptureOptions): CaptureResult | undefined +} + +// Create the implementation +const createTypedPostHog = (original: OriginalPostHog): TypedPostHog => { + // Create the enhanced PostHog object + const enhanced: TypedPostHog = Object.create(original) + + // Add captureTyped method + enhanced.captureTyped = function ( + event_name: K, + properties: P & Record, + options?: CaptureOptions + ): CaptureResult | undefined { + return original.capture(event_name, properties, options) + } + + // Add captureUntyped method + enhanced.captureUntyped = function ( + event_name: string, + properties?: Properties | null, + options?: CaptureOptions + ): CaptureResult | undefined { + return original.capture(event_name, properties, options) + } + + // Keep capture for backward compatibility (deprecated) + enhanced.capture = function ( + event_name: string, + properties?: Properties | null, + options?: CaptureOptions + ): CaptureResult | undefined { + console.warn( + 'posthog.capture() is deprecated. Use captureTyped() for type safety or captureUntyped() for flexibility.' + ) + return original.capture(event_name, properties, options) + } + + // Proxy to delegate all other properties/methods to the original + return new Proxy(enhanced, { + get(target, prop) { + if (prop in target) { + return (target as any)[prop] + } + return (original as any)[prop] + }, + set(target, prop, value) { + ;(original as any)[prop] = value + return true + }, + }) +} + +// Create and export the typed instance +const posthog = createTypedPostHog(originalPostHog as OriginalPostHog) + +export default posthog +export { posthog } +export type { EventSchemas, TypedPostHog } + +// Re-export everything else from posthog-js +export * from 'posthog-js' + +/** + * MIGRATION GUIDE + * =============== + * + * Old code: + * posthog.capture('Product Added', { product_id: '123', name: 'Widget', price: 42, quantity: 1 }) + * + * New code (with type safety): + * posthog.captureTyped('Product Added', { product_id: '123', name: 'Widget', price: 42, quantity: 1 }) + * + * For untyped/dynamic events: + * posthog.captureUntyped('Custom Event', { any: 'data' }) + */ diff --git a/packages/browser/test-captureTyped-simple.ts b/packages/browser/test-captureTyped-simple.ts new file mode 100644 index 0000000000..4f3cea5d6d --- /dev/null +++ b/packages/browser/test-captureTyped-simple.ts @@ -0,0 +1,82 @@ +/* eslint-disable no-console */ +/** + * Test that captureTyped() provides type safety and captureUntyped() is flexible + */ + +import posthog from './playground/nextjs/src/posthog-typed' + +// ======================================== +// TEST: captureTyped() with type safety +// ======================================== + +// ✅ Should work - correct properties +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42.99, + quantity: 1, +}) + +// ✅ Should work - additional properties allowed +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42, + quantity: 1, + custom_field: 'extra data', +}) + +// ❌ Should error - missing required properties +// @ts-expect-error - Missing required properties: name, price, quantity +posthog.captureTyped('Product Added', { + product_id: '123', +}) + +// ❌ Should error - wrong type for price +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + // @ts-expect-error - Type 'string' is not assignable to type 'number' + price: 'not-a-number', + quantity: 1, +}) + +// ❌ Should error - unknown event name +// @ts-expect-error - Argument of type '"Unknown Event"' is not assignable +posthog.captureTyped('Unknown Event', { + some: 'data', +}) + +// ======================================== +// TEST: captureUntyped() is flexible +// ======================================== + +// ✅ All of these should work with captureUntyped +posthog.captureUntyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42, + quantity: 1, +}) + +posthog.captureUntyped('Product Added', { + product_id: '123', + // Missing properties is OK with untyped +}) + +posthog.captureUntyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 'string is OK here', + quantity: '1', +}) + +posthog.captureUntyped('Any Random Event Name', { + any: 'properties', + work: 'here', +}) + +posthog.captureUntyped('Simple Event') +posthog.captureUntyped('Event With Null', null) + +console.log('Type checking tests complete!') diff --git a/packages/browser/typed-capture-solution.md b/packages/browser/typed-capture-solution.md new file mode 100644 index 0000000000..7dea99a1f6 --- /dev/null +++ b/packages/browser/typed-capture-solution.md @@ -0,0 +1,116 @@ +# PostHog Typed Capture Solution + +## Overview +This solution provides type-safe event capture for PostHog by introducing two new methods: +- `captureTyped()` - Type-safe capture for defined events +- `captureUntyped()` - Flexible capture for any events (original behavior) + +## Implementation Details + +### Generated File Structure +The PostHog CLI generates a `posthog-typed.ts` file that: +1. Defines event schemas directly (no module augmentation needed) +2. Creates a wrapper around the original PostHog instance +3. Implements both typed and untyped capture methods +4. Maintains backward compatibility with deprecated `capture()` method + +### Key Features + +#### Type-Safe Capture +```typescript +// ✅ Correct - all required properties with correct types +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42, + quantity: 1 +}) + +// ✅ Additional properties are allowed +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42, + quantity: 1, + custom_field: 'extra data' +}) + +// ❌ TypeScript Error - missing required properties +posthog.captureTyped('Product Added', { + product_id: '123' + // Missing: name, price, quantity +}) + +// ❌ TypeScript Error - wrong type +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 'not-a-number', // Error: string not assignable to number + quantity: 1 +}) + +// ❌ TypeScript Error - unknown event +posthog.captureTyped('Unknown Event', {}) +``` + +#### Flexible Capture +```typescript +// All of these work with captureUntyped +posthog.captureUntyped('Any Event Name', { any: 'properties' }) +posthog.captureUntyped('Product Added', { partial: 'data' }) +posthog.captureUntyped('Simple Event') +``` + +## Technical Approach + +### Why This Works +- Avoids TypeScript's method overload limitations by using separate method names +- Generic constraints (`K extends keyof EventSchemas`) work properly with different method names +- Intersection type (`P & Record`) allows additional properties while enforcing required ones + +### Type Definition +```typescript +interface TypedPostHog extends Omit { + captureTyped( + event_name: K, + properties: P & Record, + options?: CaptureOptions + ): CaptureResult | undefined + + captureUntyped( + event_name: string, + properties?: Properties | null, + options?: CaptureOptions + ): CaptureResult | undefined +} +``` + +## Migration Guide + +### For New Code +Use `captureTyped()` for type safety: +```typescript +posthog.captureTyped('Product Added', { + product_id: '123', + name: 'Widget', + price: 42, + quantity: 1 +}) +``` + +### For Dynamic Events +Use `captureUntyped()` when event names or properties are dynamic: +```typescript +const eventName = getUserEvent() +posthog.captureUntyped(eventName, dynamicProperties) +``` + +### Deprecation Notice +The original `capture()` method logs a deprecation warning directing users to use the new methods. + +## Benefits +1. **Immediate IDE feedback** - TypeScript errors appear as you type +2. **Prevents misconfigured events** - Required properties enforced at compile time +3. **Backward compatible** - Existing code continues to work +4. **Flexible when needed** - `captureUntyped()` available for dynamic scenarios +5. **No runtime overhead** - All checking happens at compile time \ No newline at end of file