Skip to content

Automattic/EventHorizon

Repository files navigation

Event Horizon

A multi-language code generation tool for type-safe event tracking. The system takes YAML schema definitions as input and generates equivalent tracking implementations for Kotlin, Swift, TypeScript, and JSON schema formats.

Installation

You can install Event Horizon using Homebrew on both macOS and Linux:

brew tap automattic/build-tools
brew install automattic/build-tools/event-horizon

If you prefer, or if Homebrew is not available, you can download prebuilt binaries directly from the releases page for both Linux amd64 and Mac arm64.

Input Schema

# Required key defining schema's format version.
schemaVersion: 1

# List of platforms that are available for code generation.
platforms:
  - android
  - ios
  - web
  - desktop

# List of groups to categorize events.
groups:
  # 'ungrouped' key is a reserved keyword.
  group_a:
    # Optional key.
    # If key is not present name is derived from the group key.
    name: Some name
    # Optional key.
    description: Some text

# List of events
events:
  user_signup:
    # Optional key.
    # '_metadata' is a reserved keyword and cannot be used as a property
    _metadata:
      # Optional key.
      description: Some description
      # Optional key.
      # Reference to a group defined in groups list.
      # If key is not present the event will be categorized as 'ungrouped'.
      group: group_a
      # Optional key.
      # List of platforms for which event should not be generated. Must use predeclared platforms.
      # If key is not present event will be generated for all platforms.
      excludedPlatforms:
        - android
        - web
    # Optional properties used with an event
    user_id:
      # Required. Type of the property for the generated code.
      # Must be one of [text, boolean, number, <predeclared enum reference>].
      type: text
      # Optional key.
      description: Some description
      # Optional key.
      # Defines if a property can be null. Must be either a boolean or a list of predeclared platforms.
      # If key is not present property is assumed to be not null.
      optional: true
    signup_provider:
      type: signup_type
      description: Some description
      optional:
        - android
        - ios

# List of enums used for property types
enums:
  signup_type:
    - google
    - facebook
    - apple

CLI

The CLI supports two primary operation modes:

  • Verification Mode: Validates input YAML schema without generating code.
  • Generation Mode: Parses input and generates code using the specified format and platform.
Option Short Description Required
--input-file -i Input schema file Yes
--output-path -o Output path used for generated files Yes (for generation)
--output-platform -p Output platform for code generation Conditional*
--output-format -f Format: kotlin, swift, ts, json Yes (for generation)
--namespace -n Namespace used for generated code No
--verify -v Only run input file verification No
--help -h Show help message and exit No

*Required for generation when schema declares availablePlatforms and format is not json.

Generated code

Event Horizon generates compact code that can be integrated with external analytics tools.

Kotlin

Generated code:

class EventHorizon(
  private val eventSink: (String, Map<String, Any>) -> Unit,
) {
  fun track(event: Trackable) {
    eventSink(event.trackableName, event.trackableProperties)
  }
}

interface Trackable {
  val trackableName: String
  val trackableProperties: Map<String, Any>
}

data class UpNextQueueReorderedEvent(
  companion object {
    const val EventName: String = "up_next_queue_reordered"
  }

  val direction: QueueDirection,
  /**
   * The number of slots the episode was moved
   */
  val slots: Number?,
  /**
   * Whether the episode was moved to the next item that will play
   */
  val isNext: Boolean,
  val source: String,
) : Trackable {
  override val trackableName: String
    get() = EventName

  override val trackableProperties: Map<String, Any>
    get() = buildMap<String, Any> {
      put("direction", direction)
      if (slots != null) {
        put("slots", slots)
      }
      put("is_next", isNext)
      put("source", source)
    }
}

enum class QueueDirection {
  Up {
    override fun toString(): String = "up"
  },
  Down {
    override fun toString(): String = "down"
  },
}

Integration and usage:

val tracker: AnalyticsTracker = TODO()
val eventHorizon = EventHorizon { eventName, eventProperties ->
  // delegation to analytics tracker
}


val event = UpNextQueueReorderedEvent(
  direction = QueueDirection.Up,
  slots = 2,
  isNext = false,
  episodeUuid = episode.uuid,
)
eventHorizon.track(event)

Swift

Generated code:

class EventHorizon {
  private let eventSink: (String, [AnyHashable : Any]) -> Void

  init(eventSink: @escaping (String, [AnyHashable : Any]) -> Void) {
    self.eventSink = eventSink
  }

  func track(_ event: Trackable) {
    eventSink(event.trackableName, event.trackableProperties)
  }
}

protocol Trackable {
  var trackableName: String { get }
  var trackableProperties: [AnyHashable : Any] { get }
}

/**
 * When the user moves (up or down) one of the episodes
 */
struct UpNextQueueReorderedEvent: Trackable {
  static let eventName: String = "up_next_queue_reordered"

  let direction: QueueDirection
  /**
   * The number of slots the episode was moved
   */
  let slots: (any Numeric)?
  /**
   * Whether the episode was moved to the next item that will play
   */
  let isNext: Bool
  let episodeUuid: String

  var trackableName: String {
    return UpNextQueueReorderedEvent.eventName
  }

  var trackableProperties: [AnyHashable : Any] {
    var props: [AnyHashable : Any] = [:]
    props["direction"] = direction.analyticsValue
    if let slots = slots {
      props["slots"] = slots
    }
    props["is_next"] = isNext
    props["episode_uuid"] = episodeUuid
    return props
  }

  init(
    direction: QueueDirection,
    slots: (any Numeric)?,
    isNext: Bool,
    episodeUuid: String
  ) {
    self.direction = direction
    self.slots = slots
    self.isNext = isNext
    self.episodeUuid = episodeUuid
  }
}

enum QueueDirection: String {
  case up = "up"
  case down = "down"

  var analyticsValue: String {
    return rawValue
  }
}

Integration and usage:

let tracker: AnalyticsTracker = TODO()
let eventHorizon = EventHorizon { eventName, eventProperties ->
   // delegation to analytics tracker
}


let event = UpNextQueueReorderedEvent(
  direction: .up,
  slots: 2,
  isNext: false,
  episodeUuid: episode.uuid,
)
eventHorizon.track(event)

TypeScript

Generated code:

export type Trackable = {
  // When the user moves (up or down) one of the episodes
  "up_next_queue_reordered": {
    direction: QueueDirection;
    // The number of slots the episode was moved
    slots?: number;
    // Whether the episode was moved to the next item that will play
    is_next: boolean;
    episode_uuid: string;
  };
};

export type QueueDirection =
  | "up"
  | "down";

Integration and usage:

const tracker = TODO()

function trackEvent<K extends keyof Trackable>(
  event: K,
  props: Trackable[K] extends undefined ? never : Trackable[K],
): void;

function trackEvent<K extends keyof Trackable>(event: K): void;

function trackEvent<K extends keyof Trackable>(event: K, props?: Trackable[K]): void {
  // delegation to analytics tracker
}


trackEvent(
  "up_next_queue_reordered",
  {
    direction: "up",
    slots: 2,
    is_next: false,
    episode_uuid: episode.uuid,
  }
)

About

A multi-language code generation tool for type-safe event tracking.

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •