Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
Expand Down
5 changes: 5 additions & 0 deletions packages/browser/src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ import {
isArray,
isEmptyObject,
isObject,
TypedEventCapture,
createTypedEventCapture,
} from '@posthog/core'
import { uuidv7 } from './uuidv7'
import { WebExperiments } from './web-experiments'
Expand Down Expand Up @@ -340,6 +342,8 @@ export class PostHog {
SentryIntegration: typeof SentryIntegration
sentryIntegration: (options?: SentryIntegrationOptions) => ReturnType<typeof sentryIntegration>

readonly typed: TypedEventCapture<this>

private _internalEventEmitter = new SimpleEventEmitter()

// Legacy property to support existing usage - this isn't technically correct but it's what it has always been - a proxy for flags being loaded
Expand Down Expand Up @@ -370,6 +374,7 @@ export class PostHog {
this._visibilityStateListener = null
this._initialPersonProfilesConfig = null
this._cachedPersonProperties = null
this.typed = createTypedEventCapture(this)
this.featureFlags = new PostHogFeatureFlags(this)
this.toolbar = new Toolbar(this)
this.scrollManager = new ScrollManager(this)
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { SegmentAnalytics } from './extensions/segment-integration'
import { PostHog } from './posthog-core'
import { KnownUnsafeEditableEvent } from '@posthog/core'
import { Survey } from './posthog-surveys-types'

// Re-export PostHogEventSchemas from @posthog/core for typed events
export type { PostHogEventSchemas } from '@posthog/core'
// only importing types here, so won't affect the bundle
// eslint-disable-next-line posthog-js/no-external-replay-imports
import type { SAMPLED } from './extensions/replay/external/triggerMatching'
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { uuidv7 } from './vendor/uuidv7'
export * from './posthog-core'
export * from './posthog-core-stateless'
export * from './types'
export * from './typed-events'
7 changes: 7 additions & 0 deletions packages/core/src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Compression, PostHogPersistedProperty } from './types'
import { maybeAdd, PostHogCoreStateless, QuotaLimitedFeature } from './posthog-core-stateless'
import { uuidv7 } from './vendor/uuidv7'
import { isPlainError } from './utils'
import { TypedEventCapture, createTypedEventCapture } from './typed-events'

export abstract class PostHogCore extends PostHogCoreStateless {
// options
Expand All @@ -40,6 +41,9 @@ export abstract class PostHogCore extends PostHogCoreStateless {
private _sessionMaxLengthSeconds: number = 24 * 60 * 60 // 24 hours
protected sessionProps: PostHogEventProperties = {}

// Typed event capture instance
public readonly typed: TypedEventCapture<this>

constructor(apiKey: string, options?: PostHogCoreOptions) {
// Default for stateful mode is to not disable geoip. Only override if explicitly set
const disableGeoipOption = options?.disableGeoip ?? false
Expand All @@ -51,6 +55,9 @@ export abstract class PostHogCore extends PostHogCoreStateless {

this.sendFeatureFlagEvent = options?.sendFeatureFlagEvent ?? true
this._sessionExpirationTimeSeconds = options?.sessionExpirationTimeSeconds ?? 1800 // 30 minutes

// Initialize typed event capture
this.typed = createTypedEventCapture(this)
}

protected setupBootstrap(options?: Partial<PostHogCoreOptions>): void {
Expand Down
51 changes: 51 additions & 0 deletions packages/core/src/typed-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Typed event capture infrastructure for PostHog.
*
* Event schemas can be augmented in PostHogEventSchemas interface via module augmentation.
* This is typically done by generated types from `posthog-cli schema pull`.
*
* @example
* // In your types file or generated file:
* declare module '@posthog/core' {
* interface PostHogEventSchemas {
* 'user_signed_up': { plan: string; trial: boolean }
* 'purchase_completed': { amount: number; currency: string }
* }
* }
*
* // Usage:
* posthog.typed.user_signed_up({ plan: 'pro', trial: true })
* posthog.typed.purchase_completed({ amount: 99.99, currency: 'USD' })
*/

import type { PostHogEventSchemas } from './types'

// Utility type that allows schema properties plus any additional properties
// The schema properties are strictly typed, additional ones are any
export type EventWithAdditionalProperties<T> = T & Record<string, any>

/**
* Mapped type that creates typed methods for each event in PostHogEventSchemas.
* Methods are generated dynamically based on the augmented interface.
*/
export type TypedEventCapture<Client extends { capture: (event: string, properties?: any) => any }> = {
[K in keyof PostHogEventSchemas]: (
properties: EventWithAdditionalProperties<PostHogEventSchemas[K]>
) => ReturnType<Client['capture']>
}

/**
* Creates a Proxy that dynamically generates typed event methods
* based on the PostHogEventSchemas interface.
*/
export function createTypedEventCapture<Client extends { capture: (event: string, properties?: any) => any }>(
client: Client
): TypedEventCapture<Client> {
return new Proxy({} as TypedEventCapture<Client>, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Proxy is not supported in IE 11, which is listed in the browserslist. This will cause runtime errors in IE 11 environments when the typed property is accessed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/typed-events.ts
Line: 44:44

Comment:
**logic:** `Proxy` is not supported in IE 11, which is listed in the browserslist. This will cause runtime errors in IE 11 environments when the `typed` property is accessed.

How can I resolve this? If you propose a fix, please make it concise.

get: (_target, eventName: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The eventName parameter should be typed as string | symbol since Proxy get traps can receive Symbols (e.g., for well-known symbols like Symbol.toStringTag). Consider adding a type guard to filter out non-string property keys.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/typed-events.ts
Line: 45:45

Comment:
**logic:** The `eventName` parameter should be typed as `string | symbol` since Proxy get traps can receive Symbols (e.g., for well-known symbols like `Symbol.toStringTag`). Consider adding a type guard to filter out non-string property keys.

How can I resolve this? If you propose a fix, please make it concise.

return (properties: any) => {
return client.capture(eventName, properties)
}
},
})
}
6 changes: 6 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ export type PostHogEventProperties = {
[key: string]: JsonType
}

// Event schemas can be augmented by generated types from posthog-cli schema pull
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PostHogEventSchemas {
// This interface is intentionally empty and should be augmented by generated types
}

export type PostHogGroupProperties = {
[type: string]: string | number
}
Expand Down