Skip to content

Automattic/EventHorizon

Repository files navigation

Event Horizon

A multi-language code generation tool for type-safe event tracking. It takes YAML schema definitions as input and generates corresponding tracking implementations for Kotlin, Swift, TypeScript, and JSON Schema.

Installation

You can install Event Horizon with 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 unavailable, you can download prebuilt binaries directly from the releases page for Linux amd64 and macOS arm64.

Input Schema

# Required key that defines the schema format version.
schemaVersion: 1

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

# List of groups used to categorize events.
groups:
  # 'ungrouped' is a reserved key.
  group_a:
    # Optional key.
    # If omitted, the name is derived from the group key.
    name: Some name
    # Optional key.
    description: Some description

# 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 the groups list.
      # If omitted, the event is categorized as 'ungrouped'.
      group: group_a
      # Optional key.
      # List of platforms for which the event should be generated. Must use declared platforms.
      # If omitted, the event is generated for all platforms.
      # If empty, the event is not generated for any platform.
      includedPlatforms:
        - android
        - web
    # Optional properties associated with the event.
    user_id:
      # Required. Property type used in generated code.
      # Must be one of [text, boolean, int, float, <declared enum reference>].
      type: text
      # Optional key.
      description: Some description
      # Optional key.
      # Defines whether a property can be null. Must be either a boolean or a list of declared platforms.
      # If omitted, the property is assumed to be non-null.
      optional: true
    signup_provider:
      type: signup_type
      description: Some description
      optional:
        - android
        - ios

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

# List of disallowed property names.
# Prefix an entry with 'predefined:' to use a predefined rule set, for example 'predefined:tracks'.
# Currently supported predefined rule sets:
#   - tracks
reservedProperties:
  - property_name_1
  - property_name_2
  - predefined:<value>

CLI

The CLI supports two primary modes of operation:

  • Verification mode: validates the input YAML schema without generating code.
  • Generation mode: parses the input and generates code for the specified format and platform.
Option Short Description Required
--input-file -i Input schema file Yes
--output-path -o Output path for generated files Yes (for generation)
--output-platform -p Target platform for code generation Conditional*
--output-format -f Format: kotlin, swift, ts, json Yes (for generation)
--namespace -n Namespace for generated code No
--verify -v Run input file validation only No
--help -h Show help and exit No

*Required for generation when the schema declares platforms and the 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: (Trackable) -> Unit,
) {
  fun track(event: Trackable) {
    eventSink(event)
  }
}

interface Trackable : Parcelable {
  val analyticsName: String
  val analyticsProperties: Map<String, Any>
}

/**
 * Emitted when the user moves an episode up or down.
 */
@Parcelize
data class UpNextQueueReorderedEvent(
  companion object {
    const val EventName: String = "up_next_queue_reordered"
  }

  val direction: QueueDirection,
  /**
   * Whether the episode was moved into the next slot to play.
   */
  val isNext: Boolean,
  val episodeUuid: String,
  /**
   * The number of positions the episode was moved.
   */
  val slots: Long? = null,
) : Trackable {
  @IgnoredOnParcel
  override val analyticsName: String
    get() = EventName

  @IgnoredOnParcel
  override val analyticsProperties: Map<String, Any> = buildMap<String, Any> {
    put("direction", direction.toString())
    put("is_next", isNext)
    put("episode_uuid", episodeUuid)
    if (slots != null) {
      put("slots", slots)
    }
  }
}

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

Integration example:

val tracker: AnalyticsTracker = TODO()
val eventHorizon = EventHorizon { event ->
  // Delegate tracking to your analytics system.
}


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

Swift

Generated code:

class EventHorizon {
  private let eventSink: (any Trackable) -> Void

  init(eventSink: @escaping (any Trackable) -> Void) {
    self.eventSink = eventSink
  }

  func track(_ event: any Trackable) {
    eventSink(event)
  }
}

protocol Trackable : Hashable, CustomStringConvertible {
  var analyticsName: String { get }
  var analyticsProperties: [String : CustomStringConvertible] { get }
}

/**
 * Emitted when the user moves an episode up or down.
 */
struct UpNextQueueReorderedEvent: Trackable {
  static let eventName: String = "up_next_queue_reordered"

  let direction: QueueDirection
  /**
   * Whether the episode was moved into the next slot to play.
   */
  let isNext: Bool
  let episodeUuid: String
  /**
   * The number of positions the episode was moved.
   */
  let slots: Int?

  var analyticsName: String {
    return UpNextQueueReorderedEvent.eventName
  }

  let analyticsProperties: [String : CustomStringConvertible]

  init(
    direction: QueueDirection,
    isNext: Bool,
    episodeUuid: String,
    slots: Int? = nil
  ) {
    self.direction = direction
    self.isNext = isNext
    self.episodeUuid = episodeUuid
    self.slots = slots

    var _props: [String : CustomStringConvertible] = [:]
    _props["direction"] = direction.analyticsValue
    _props["is_next"] = isNext
    _props["episode_uuid"] = episodeUuid
    if let slots = slots {
      _props["slots"] = slots
    }
    self.analyticsProperties = _props
  }

  public static func == (lhs: UpNextQueueReorderedEvent, rhs: UpNextQueueReorderedEvent) -> Bool {
    return
      lhs.direction == rhs.direction &&
      lhs.isNext == rhs.isNext &&
      lhs.episodeUuid == rhs.episodeUuid &&
      lhs.slots == rhs.slots
  }

  public func hash(into hasher: inout Hasher) {
    hasher.combine(direction)
    hasher.combine(isNext)
    hasher.combine(episodeUuid)
    hasher.combine(slots)
  }

  public var description: String {
    var parts: [String] = []
    parts.append("direction: \(direction)")
    parts.append("isNext: \(isNext)")
    parts.append("episodeUuid: \(episodeUuid)")
    parts.append("slots: \(String(describing: slots))")
    return "UpNextQueueReorderedEvent(\(parts.joined(separator: ", ")))"
  }
}

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

  var analyticsValue: String {
    return rawValue
  }
}

Integration example:

let tracker: AnalyticsTracker = TODO()
let eventHorizon = EventHorizon { event in
   // Delegate tracking to your analytics system.
}


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

TypeScript

Generated code:

export type Trackable = {
  // Emitted when the user moves an episode up or down.
  "up_next_queue_reordered": {
    direction: QueueDirection;
    // The number of positions the episode was moved.
    slots?: number;
    // Whether the episode was moved into the next slot to play.
    is_next: boolean;
    episode_uuid: string;
  };
};

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

Integration example:

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 {
  // Delegate tracking to your analytics system.
}


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