|
1 | | -from datetime import datetime |
2 | 1 | from typing import Any, Literal, Optional, cast |
3 | 2 |
|
4 | 3 | from django.core.cache import cache |
|
10 | 9 |
|
11 | 10 | from posthog.api.event_definition_generators.base import EventDefinitionGenerator |
12 | 11 | from posthog.api.event_definition_generators.golang import GolangGenerator |
| 12 | +from posthog.api.event_definition_generators.typescript import TypeScriptGenerator |
13 | 13 | from posthog.api.routing import TeamAndOrgViewSetMixin |
14 | 14 | from posthog.api.shared import UserBasicSerializer |
15 | 15 | from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin |
@@ -402,260 +402,28 @@ def destroy(self, request: request.Request, *args: Any, **kwargs: Any) -> respon |
402 | 402 |
|
403 | 403 | @action(detail=False, methods=["GET"], url_path="typescript", required_scopes=["event_definition:read"]) |
404 | 404 | 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()) |
632 | 406 |
|
633 | 407 | @action(detail=False, methods=["GET"], url_path="golang", required_scopes=["event_definition:read"]) |
634 | 408 | def golang_definitions(self, *args, **kwargs): |
635 | | - """Generate Go code definitions from event schemas""" |
636 | | - generator = GolangGenerator() |
| 409 | + return self._generate_definitions(GolangGenerator()) |
637 | 410 |
|
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) |
640 | 413 |
|
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) |
643 | 415 |
|
644 | | - # Generate Go code using the generator |
645 | | - go_content = generator.generate(event_definitions, schema_map) |
| 416 | + content = generator.generate(event_definitions, schema_map) |
646 | 417 |
|
647 | | - # Report telemetry for this generation request |
648 | 418 | generator.record_report_generation( |
649 | | - generator.language_name(), |
650 | | - generator.generator_version(), |
651 | 419 | self.request.user, |
652 | 420 | self.team_id, |
653 | 421 | self.project_id, |
654 | 422 | ) |
655 | 423 |
|
656 | 424 | return response.Response( |
657 | 425 | { |
658 | | - "content": go_content, |
| 426 | + "content": content, |
659 | 427 | "event_count": len(event_definitions), |
660 | 428 | "schema_hash": schema_hash, |
661 | 429 | "generator_version": generator.generator_version(), |
|
0 commit comments