Skip to content

Commit a3b448c

Browse files
authored
chore: Refactor the TypeScript generator to share the logic with the Golang generator (#42129)
1 parent af012c0 commit a3b448c

File tree

5 files changed

+247
-273
lines changed

5 files changed

+247
-273
lines changed

posthog/api/event_definition.py

Lines changed: 8 additions & 240 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from datetime import datetime
21
from typing import Any, Literal, Optional, cast
32

43
from django.core.cache import cache
@@ -10,6 +9,7 @@
109

1110
from posthog.api.event_definition_generators.base import EventDefinitionGenerator
1211
from posthog.api.event_definition_generators.golang import GolangGenerator
12+
from posthog.api.event_definition_generators.typescript import TypeScriptGenerator
1313
from posthog.api.routing import TeamAndOrgViewSetMixin
1414
from posthog.api.shared import UserBasicSerializer
1515
from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin
@@ -402,260 +402,28 @@ def destroy(self, request: request.Request, *args: Any, **kwargs: Any) -> respon
402402

403403
@action(detail=False, methods=["GET"], url_path="typescript", required_scopes=["event_definition:read"])
404404
def typescript_definitions(self, *args, **kwargs):
405-
"""Generate TypeScript definitions from event schemas"""
406-
# Version of the TypeScript generator - increment when changing the structure
407-
# This ensures clients update even when schemas don't change
408-
# TODO migrate to its own custom generator.
409-
generator_version = "1.0.0"
410-
language = "TypeScript"
411-
412-
# Fetch event definitions and schemas using shared helper
413-
event_definitions, schema_map = EventDefinitionGenerator.fetch_event_definitions_and_schemas(self.project_id)
414-
415-
# Calculate the deterministic hash for this update
416-
schema_hash = EventDefinitionGenerator.calculate_schema_hash(
417-
generator_version,
418-
event_definitions,
419-
schema_map,
420-
)
421-
422-
# Generate TypeScript definitions
423-
ts_content = self._generate_typescript(generator_version, event_definitions, schema_map)
424-
425-
# Report telemetry for this generation request
426-
EventDefinitionGenerator.record_report_generation(
427-
language, generator_version, self.request.user, self.team_id, self.project_id
428-
)
429-
430-
return response.Response(
431-
{
432-
"content": ts_content,
433-
"event_count": len(event_definitions),
434-
"schema_hash": schema_hash,
435-
"generator_version": generator_version,
436-
}
437-
)
438-
439-
def _generate_typescript(self, generator_version: str, event_definitions, schema_map):
440-
"""Generate complete TypeScript module with type definitions and exports"""
441-
# Generate file header
442-
header = f"""/**
443-
* GENERATED FILE - DO NOT EDIT
444-
*
445-
* This file was auto-generated by PostHog
446-
* Generated at: {datetime.now().isoformat()}
447-
* Generator version: {generator_version}
448-
*
449-
* Provides capture() for type-safe events and captureRaw() for flexibility
450-
*/
451-
import originalPostHog from 'posthog-js'
452-
import type {{ CaptureOptions, CaptureResult, PostHog as OriginalPostHog, Properties }} from 'posthog-js'
453-
"""
454-
455-
# Generate event schemas interface
456-
event_schemas_lines = [
457-
"// Define event schemas with their required and optional fields",
458-
"interface EventSchemas {",
459-
]
460-
461-
for event_def in event_definitions:
462-
properties = schema_map.get(str(event_def.id), [])
463-
event_name_json = orjson.dumps(event_def.name).decode("utf-8")
464-
465-
if not properties:
466-
event_schemas_lines.append(f" {event_name_json}: Record<string, any>")
467-
else:
468-
event_schemas_lines.append(f" {event_name_json}: {{")
469-
for prop in properties:
470-
ts_type = self._map_property_type(prop.property_type)
471-
optional_marker = "" if prop.is_required else "?"
472-
# Use orjson.dumps() for proper escaping of property names
473-
prop_name_json = orjson.dumps(prop.name).decode("utf-8")
474-
event_schemas_lines.append(f" {prop_name_json}{optional_marker}: {ts_type}")
475-
event_schemas_lines.append(" }")
476-
477-
event_schemas_lines.append("}")
478-
event_schemas = "\n".join(event_schemas_lines)
479-
480-
# Generate type aliases
481-
type_aliases = """
482-
// Type alias for all valid event names
483-
export type EventName = keyof EventSchemas
484-
485-
// Type helper to get properties for a specific event
486-
// Intersects the schema with Record<string, any> to allow additional properties
487-
export type EventProperties<K extends EventName> = EventSchemas[K] & Record<string, any>
488-
489-
// Helper type to check if a type has required properties
490-
type HasRequiredProperties<K extends EventName> = {} extends EventSchemas[K] ? false : true
491-
492-
// Helper to detect if T is exactly 'string' (not a literal)
493-
type IsExactlyString<T> = string extends T ? (T extends string ? true : false) : false
494-
"""
495-
496-
# Generate TypedPostHog interface
497-
typed_posthog_interface = """
498-
// Enhanced PostHog interface with typed capture
499-
interface TypedPostHog extends Omit<OriginalPostHog, 'capture'> {
500-
/**
501-
* Type-safe capture for defined events, or flexible capture for undefined events
502-
*
503-
* Note: For defined events, wrap properties in a variable to allow additional properties:
504-
* const props = { file_size_b: 100, extra: 'data' }
505-
* posthog.capture('downloaded_file', props)
506-
*
507-
* @example
508-
* // Defined event with type safety
509-
* posthog.capture('uploaded_file', {
510-
* file_name: 'test.txt',
511-
* file_size_b: 100
512-
* })
513-
*
514-
* @example
515-
* // For events with all optional properties, properties argument is optional
516-
* posthog.capture('logged_out') // no properties needed
517-
*
518-
* @example
519-
* // Undefined events work with arbitrary properties
520-
* posthog.capture('custom_event', { whatever: 'data' })
521-
* posthog.capture('another_event') // or no properties
522-
*/
523-
// Overload 1: For known events (specific EventName literals)
524-
// This should match first for all known event names
525-
capture<K extends EventName>(
526-
event_name: K,
527-
...args: HasRequiredProperties<K> extends true
528-
? [properties: EventProperties<K>, options?: CaptureOptions]
529-
: [properties?: EventProperties<K>, options?: CaptureOptions]
530-
): CaptureResult | undefined
531-
532-
// Overload 2: For undefined events and blocking string variables
533-
// Only matches if event_name is NOT a known EventName
534-
// The conditional type rejects broad string type
535-
capture<T extends string>(
536-
event_name: IsExactlyString<T> extends true ? never : (T extends EventName ? never : T),
537-
properties?: Properties | null,
538-
options?: CaptureOptions
539-
): CaptureResult | undefined
540-
541-
/**
542-
* Raw capture for any event (original behavior, no type checking)
543-
*
544-
* Use capture() for type-safe defined events or flexible undefined events.
545-
* Use captureRaw() only when you need to bypass all type checking.
546-
*
547-
* @example
548-
* posthog.captureRaw('Any Event Name', { whatever: 'data' })
549-
*/
550-
captureRaw(event_name: string, properties?: Properties | null, options?: CaptureOptions): CaptureResult | undefined
551-
}
552-
"""
553-
554-
# Generate implementation
555-
implementation = """
556-
// Create the implementation
557-
const createTypedPostHog = (original: OriginalPostHog): TypedPostHog => {
558-
// Create the enhanced PostHog object
559-
const enhanced: TypedPostHog = Object.create(original)
560-
561-
// Add capture method (type-safe for defined events, flexible for undefined)
562-
enhanced.capture = function (event_name: string, ...args: any[]): CaptureResult | undefined {
563-
const [properties, options] = args
564-
return original.capture(event_name, properties, options)
565-
}
566-
567-
// Add captureRaw method for untyped/flexible event tracking
568-
enhanced.captureRaw = function (
569-
event_name: string,
570-
properties?: Properties | null,
571-
options?: CaptureOptions
572-
): CaptureResult | undefined {
573-
return original.capture(event_name, properties, options)
574-
}
575-
576-
// Proxy to delegate all other properties/methods to the original
577-
return new Proxy(enhanced, {
578-
get(target, prop) {
579-
if (prop in target) {
580-
return (target as any)[prop]
581-
}
582-
return (original as any)[prop]
583-
},
584-
set(target, prop, value) {
585-
;(original as any)[prop] = value
586-
return true
587-
},
588-
})
589-
}
590-
"""
591-
592-
# Generate exports
593-
exports = """
594-
// Create and export the typed instance
595-
const posthog = createTypedPostHog(originalPostHog as OriginalPostHog)
596-
597-
export default posthog
598-
export type { EventSchemas, TypedPostHog }
599-
600-
// Re-export everything else from posthog-js
601-
export * from 'posthog-js'
602-
603-
/**
604-
* USAGE GUIDE
605-
* ===========
606-
*
607-
* For type-safe defined events (recommended):
608-
* posthog.capture('uploaded_file', { file_name: 'test.txt', file_size_b: 100 })
609-
*
610-
* For undefined events (flexible):
611-
* posthog.capture('Custom Event', { whatever: 'data' })
612-
*
613-
* For bypassing all type checking (rare):
614-
* posthog.captureRaw('Any Event', { whatever: 'data' })
615-
*/
616-
"""
617-
618-
# Combine all sections
619-
return header + event_schemas + type_aliases + typed_posthog_interface + implementation + exports
620-
621-
def _map_property_type(self, property_type: str) -> str:
622-
"""Map PostHog property types to TypeScript types"""
623-
type_map = {
624-
"String": "string",
625-
"Numeric": "number",
626-
"Boolean": "boolean",
627-
"DateTime": "string | Date",
628-
"Array": "any[]",
629-
"Object": "Record<string, any>",
630-
}
631-
return type_map.get(property_type, "any")
405+
return self._generate_definitions(TypeScriptGenerator())
632406

633407
@action(detail=False, methods=["GET"], url_path="golang", required_scopes=["event_definition:read"])
634408
def golang_definitions(self, *args, **kwargs):
635-
"""Generate Go code definitions from event schemas"""
636-
generator = GolangGenerator()
409+
return self._generate_definitions(GolangGenerator())
637410

638-
# Fetch event definitions and schemas using shared helper
639-
event_definitions, schema_map = EventDefinitionGenerator.fetch_event_definitions_and_schemas(self.project_id)
411+
def _generate_definitions(self, generator: EventDefinitionGenerator) -> response.Response:
412+
event_definitions, schema_map = generator.fetch_event_definitions_and_schemas(self.project_id)
640413

641-
# Calculate the deterministic hash for this update
642-
schema_hash = generator.calculate_schema_hash(generator.generator_version(), event_definitions, schema_map)
414+
schema_hash = generator.calculate_schema_hash(event_definitions, schema_map)
643415

644-
# Generate Go code using the generator
645-
go_content = generator.generate(event_definitions, schema_map)
416+
content = generator.generate(event_definitions, schema_map)
646417

647-
# Report telemetry for this generation request
648418
generator.record_report_generation(
649-
generator.language_name(),
650-
generator.generator_version(),
651419
self.request.user,
652420
self.team_id,
653421
self.project_id,
654422
)
655423

656424
return response.Response(
657425
{
658-
"content": go_content,
426+
"content": content,
659427
"event_count": len(event_definitions),
660428
"schema_hash": schema_hash,
661429
"generator_version": generator.generator_version(),

posthog/api/event_definition_generators/base.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,12 @@ def generate(
3838
"""
3939
pass
4040

41-
@abstractmethod
42-
def map_property_type(self, property_type: str) -> str:
43-
"""
44-
Map PostHog property types to target language types.
45-
Implement this in subclasses for language-specific type mappings.
46-
"""
47-
pass
48-
49-
@staticmethod
5041
def calculate_schema_hash(
51-
generator_version: str,
42+
self,
5243
event_definitions: QuerySet[EventDefinition],
5344
schema_map: dict[str, list[SchemaPropertyGroupProperty]],
5445
) -> str:
5546
"""
56-
Note: This is a `staticmethod` temporarily only until the typescript generator has been migrated to this
57-
setup as well.
58-
5947
Calculate a deterministic hash of the event schemas and generator version.
6048
The hash is used by clients to know when to regenerate their code.
6149
"""
@@ -71,41 +59,32 @@ def calculate_schema_hash(
7159

7260
# Include generator version to force regeneration on structural changes
7361
hash_input = {
74-
"version": generator_version,
62+
"version": self.generator_version(),
7563
"schemas": schema_data,
7664
}
7765

7866
return hashlib.sha256(orjson.dumps(hash_input, option=orjson.OPT_SORT_KEYS)).hexdigest()[:32]
7967

80-
@staticmethod
81-
def record_report_generation(
82-
language_name: str, generator_version: str, user, team_id: int, project_id: int
83-
) -> None:
68+
def record_report_generation(self, user, team_id: int, project_id: int) -> None:
8469
"""
85-
Note: This is a `staticmethod` temporarily only until the typescript generator has been migrated to this
86-
setup as well.
87-
8870
A convenience method to structurally report telemetry for code generation.
8971
"""
9072
report_user_action(
9173
user,
9274
"event definitions generated",
9375
{
94-
"language": language_name,
95-
"generator_version": generator_version,
76+
"language": self.language_name(),
77+
"generator_version": self.generator_version(),
9678
"team_id": team_id,
9779
"project_id": project_id,
9880
},
9981
)
10082

101-
@staticmethod
10283
def fetch_event_definitions_and_schemas(
84+
self,
10385
project_id: int,
10486
) -> tuple[QuerySet[EventDefinition], dict[str, list[SchemaPropertyGroupProperty]]]:
10587
"""
106-
Note: This is a `staticmethod` temporarily only until the typescript generator has been migrated to this
107-
setup as well.
108-
10988
Fetch event definitions and build schema map. The key of `schema_map` references a EventDefinition.ID
11089
from the returned event_definitions set.
11190
"""

posthog/api/event_definition_generators/golang.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def _generate_option_function(
196196
used_function_names: set[str],
197197
) -> str:
198198
"""Generate a With* option function for an optional property"""
199-
param_type = self.map_property_type(prop.property_type)
199+
param_type = self._map_property_type(prop.property_type)
200200
prop_name = self._escape_go_string(prop.name)
201201
param_name = self._to_go_param_name(prop_name)
202202
suffix = self._to_pascal_name(prop_name)
@@ -228,7 +228,7 @@ def _generate_capture_function(
228228
param_name = self._get_unique_name(self._to_go_param_name(prop_name), used_param_names)
229229
props_init_lines.append(f"\t\t{prop_name}: {param_name},")
230230

231-
param_type = self.map_property_type(prop.property_type)
231+
param_type = self._map_property_type(prop.property_type)
232232
params.append(f"{param_name} {param_type}")
233233

234234
params_str = ",\n\t".join(params)
@@ -275,7 +275,7 @@ def _generate_capture_from_base_function(
275275
param_name = self._get_unique_name(self._to_go_param_name(prop_name), used_param_names)
276276
props_init_lines.append(f"\t\t{prop_name}: {param_name},")
277277

278-
param_type = self.map_property_type(prop.property_type)
278+
param_type = self._map_property_type(prop.property_type)
279279
params.append(f"{param_name} {param_type}")
280280

281281
params_str = ",\n\t".join(params)
@@ -306,7 +306,7 @@ def _generate_capture_from_base_function(
306306
}}
307307
"""
308308

309-
def map_property_type(self, property_type: str) -> str:
309+
def _map_property_type(self, property_type: str) -> str:
310310
type_map = {
311311
"String": "string",
312312
"Numeric": "float64",

0 commit comments

Comments
 (0)