ReferenceAvo CodegenLibrary codegen

Library codegen for Kotlin, Swift, and TypeScript

Avo can emit generated code in two shapes for Kotlin, Swift, and TypeScript sources: the default single-source output (one file per source) and the library mode output that splits the runtime from the per-source types. Both modes are supported — they serve different use cases and you can generate either from the same source.

Library mode is the better fit when you have multiple Avo sources sharing the same app, when you’re building an internal tracking library reused across apps, or when you want the runtime code separated from the per-source event types.


1. The shape of library mode

avo pull emits multiple files per source instead of one. The shared source-independent runtime — Avo class, validation helpers, AvoEvent protocol/interface, AvoInvoke — lives in a separate “library” file. The per-source “app” file contains only the things that change when your tracking plan changes: per-event classes and the codegen-bound IDs. Optionally, a third file is extracted with everything needed for Avo initialization, so initialization can be decoupled from the apps too.

What gets generated

  • Library file: the runtime. AvoEnv, AvoAssert, AvoInvoke, the AvoEvent protocol/interface, and the Avo class. Regenerated rarely. Every source generates an identical library file. Shared across all sources/apps. Optionally orchestrated from a single place — “the library”.
  • App file: per-source types and a thin config object. Regenerated whenever the tracking plan source changes.
  • In TypeScript, a third file AvoConfig.ts holds the source config with schemaId/actionId/branchId/sourceId plus destination API keys. It has no runtime imports — just the values.

Mental model

Events are values you construct, not method calls. You build an instance of LoginSuccessEvent(timestamp: ..., teamId: ..., ...) and pass it to avo.process(event) (Swift/Kotlin) or avo.track(event) (TypeScript). The call returns a map keyed by destination name, with the per-destination payload (event name + properties) for each one.

The boundary: the library file knows nothing about your tracking plan. The app file knows nothing about how validation or invocation metrics work. You decide what to do with the per-destination map the call returns — either fan out manually, or hand off to destination implementations the runtime calls for you.


2. Quick start

Enable library mode on a per-source basis with the --forceFeatures flag on avo pull. The flag names match the language:

avo pull --forceFeatures SwiftLibraryInterface
avo pull --forceFeatures KotlinLibraryInterface
avo pull --forceFeatures TypeScriptLibraryInterface

You can also enable these features in your Avo workspace if you’d like them on by default — reach out and we’ll switch them on for your source.

Kotlin

avo pull produces:

  • Analytics.kt — per-event data classes and AvoTrackingPlanConfig
  • AnalyticsLibraryInterface.kt — runtime (Avo class, AvoEvent, AvoAssert, AvoInvoke)

Both files share the same package (inferred from the source path, default sh.avo). Treat them like any other Kotlin files in your project — drop them anywhere in your source tree that fits your module layout.

Initialize using the initAvo extension that’s generated alongside the per-event classes:

import sh.avo.Avo
import sh.avo.AvoEnv
import sh.avo.AvoVerificationError
import sh.avo.LoginSuccessEvent
import sh.avo.initAvo
 
val avo = Avo.initAvo(env = AvoEnv.DEV)
 
try {
    val payloads = avo.process(LoginSuccessEvent(
        timestamp = 1730000000,
        teamId = "team_42",
        teamDomain = "acme.example"
    ))
 
    payloads["custom"]?.let { event ->
        myAnalyticsSdk.logEvent(event.name, event.properties)
    }
} catch (e: AvoVerificationError) {
    // Validation failed and strict mode is on.
    println("[avo] ${e.messages.joinToString(", ")}")
}

process() returns Map<String, AvoDestinationEvent> keyed by destination name (lowercased identifier from your Avo workspace). In development and staging it throws AvoVerificationError when validation fails and strict = true (the default), otherwise it logs a warning and continues. In production, runtime validation is skipped entirely — process() never throws or logs, and always returns the destination map. Returns an empty map in noop mode.

Swift

avo pull produces:

  • Analytics.swiftAvoTypes namespace, per-event structs, AvoTrackingPlanConfig, and an extension Avo with a no-config initAvo
  • AnalyticsLibraryInterface.swift — runtime

The app file imports Library. You decide where the library file lives: package it as a Swift module called Library (SPM package, Xcode target, internal framework — whichever fits your project) so the import resolves, or drop both files into the same module and remove the import Library line from the app file by hand. Either approach works; the library file is just Swift source.

Initialize:

import Library
 
let avo = Avo.initAvo(env: .dev)
 
do {
    let payloads = try avo.process(event: LoginSuccessEvent(
        timestamp: 1730000000,
        teamId: "team_42",
        teamDomain: "acme.example"
    ))
 
    if let event = payloads["custom"] {
        MyAnalyticsSDK.logEvent(name: event.name, properties: event.properties)
    }
} catch let error as AvoVerificationError {
    // Validation failed and strict mode is on.
    print("Avo verification: \(error.messages.joined(separator: \", \"))")
}

process(event:) returns [String: AvoDestinationEvent]. In development and staging it throws AvoVerificationError when validation fails and strict is on, otherwise it logs via NSLog and continues. In production, runtime validation is skipped entirely — process(event:) never throws or logs. Returns an empty dictionary in noop mode.

TypeScript

TypeScript library mode has two file layouts. By default avo pull emits one file for all events; with the “Generate one file per event” source setting on (Source → Avo Codegen Setup), each event gets its own module. Both layouts expose the same ./Avo import surface, so the init, destination, and tracking code below applies to either one.

One file for all events

avo pull produces three files:

  • Avo.ts — per-event classes (LoginSuccessEvent, etc.) extending BaseAvoEvent, plus re-exports of the public runtime surface
  • AvoLibrary.ts — runtime (Avo class, AvoEvent, AvoAssert, BaseAvoEvent)
  • AvoConfig.ts — codegen-bound config (default export), DestinationKey union, and per-destination API keys

AvoConfig.ts is the only file that contains your schemaId/actionId/branchId. Move it (and update the import path) wherever you want Avo.init to live — at the app level, a library level, or any module boundary.

There are no runtime dependencies outside the standard library and fetch (used by AvoInvoke).

Avo.init takes two arguments: a runtime AvoConfig and the codegen-bound config (default-exported from AvoConfig.ts). The recommended starting point is to wire all destinations through the runtime:

import { Avo, AvoEnv, AvoVerificationError, LoginSuccessEvent } from './Avo';
import codegenConfig from './AvoConfig';
 
const avo = Avo.init(
  {
    env: AvoEnv.Dev,
    destinations: {
      Custom: {
        make(env, apiKey) { /* initialize your SDK */ },
        logEvent(event) { myAnalyticsSDK.logEvent(event.name, event.properties); },
        setUserProperties(userId, props) { myAnalyticsSDK.identify(userId, props); },
      },
    },
  },
  codegenConfig,
);
 
try {
  avo.track(new LoginSuccessEvent(1730000000, 'team_42', 'acme.example'));
} catch (e) {
  if (e instanceof AvoVerificationError) {
    console.error('[avo]', e.messages.join(', '));
  } else {
    throw e;
  }
}

AvoCustomDestination has the same shape as the custom destination interface in the single-source output — make (optional), logEvent, setUserProperties, identify, unidentify, logPage, and revenue — so an existing single-source destination implementation is drop-in compatible. destinations is all-or-nothing: if you provide any, you must provide an implementation for every DestinationKey or Avo.init will throw.

If the all-or-nothing model doesn’t fit (for example, you want to fan out manually for some destinations), omit destinations and fan out yourself:

const avo = Avo.init({ env: AvoEnv.Dev }, codegenConfig);
 
try {
  const payloads = avo.track(new LoginSuccessEvent(1730000000, 'team_42', 'acme.example'));
 
  const custom = payloads['Custom'];
  if (custom) {
    myAnalyticsSDK.logEvent(custom.name, custom.properties);
  }
} catch (e) {
  if (e instanceof AvoVerificationError) {
    console.error('[avo]', e.messages.join(', '));
  } else {
    throw e;
  }
}

track() returns Record<string, AvoDestinationEvent> keyed by DestinationKey from AvoConfig.ts. It throws AvoVerificationError the same way Kotlin/Swift do.

One file per event

With the “Generate one file per event” source setting on, each event is split into its own module for better tree‑shaking. avo pull then emits an AvoEvents/ directory and one extra file:

One file for all events
├─ Avo.ts          # per-event classes + runtime re-exports
├─ AvoLibrary.ts   # runtime
└─ AvoConfig.ts    # codegen-bound config

One file per event
├─ Avo.ts                    # thin barrel — re-exports everything below
├─ AvoLibrary.ts             # runtime (unchanged)
├─ AvoConfig.ts              # codegen-bound config (unchanged)
├─ AvoAppSystemProperties.ts # AppSystemProperties singleton
└─ AvoEvents/
   ├─ index.ts
   ├─ types.ts               # shared object and enum types
   └─ <Event>.ts             # one module per event

The public import surface does not change. Avo.ts becomes a thin barrel that re‑exports the runtime (Avo, AvoEnv, AvoVerificationError, …), every per‑event class, AppSystemProperties, and the shared types — so import { Avo, AvoEnv, AvoVerificationError, LoginSuccessEvent } from './Avo' keeps working exactly as in the one-file layout above. You don’t need to change any call sites.

AppSystemProperties moves out of Avo.ts into its own AvoAppSystemProperties.ts file so the per‑event modules can import it from a sibling — importing it from the Avo.ts barrel would create an import cycle, since the barrel re‑exports the per‑event files. You still import AppSystemProperties from ./Avo; this is only an internal file split.


3. Differences from the single-source output

If you’re switching an existing source from the single-source output to library mode, here’s what changes — and what stays the same.

Before / after

Kotlin

// Single-source
val avo = Avo(env = AvoEnv.DEV, customDestination = MyCustomDestination())
avo.loginSuccess(
    timestamp = 1730000000,
    teamId = "team_42",
    teamDomain = "acme.example",
)
 
// Library mode
val avo = Avo.initAvo(env = AvoEnv.DEV)
try {
    val payloads = avo.process(LoginSuccessEvent(
        timestamp = 1730000000,
        teamId = "team_42",
        teamDomain = "acme.example",
    ))
    payloads["custom"]?.let { event ->
        myAnalyticsSdk.logEvent(event.name, event.properties)
    }
} catch (e: AvoVerificationError) {
    println("[avo] ${e.messages.joinToString(", ")}")
}

Swift

// Single-source
let avo = Avo(env: .dev, customDestination: MyCustomDestination())
avo.loginSuccess(
    timestamp: 1730000000,
    teamId: "team_42",
    teamDomain: "acme.example"
)
 
// Library mode
let avo = Avo.initAvo(env: .dev)
do {
    let payloads = try avo.process(event: LoginSuccessEvent(
        timestamp: 1730000000,
        teamId: "team_42",
        teamDomain: "acme.example"
    ))
    if let event = payloads["custom"] {
        MyAnalyticsSDK.logEvent(name: event.name, properties: event.properties)
    }
} catch let error as AvoVerificationError {
    print("Avo verification: \(error.messages.joined(separator: \", \"))")
}

TypeScript

// Single-source
import Avo from './Avo';
Avo.initAvo({ env: Avo.AvoEnv.Dev }, /* destinations */ {});
Avo.loginSuccess({
  timestamp: 1730000000,
  teamId: 'team_42',
  teamDomain: 'acme.example',
});
 
// Library mode
import { Avo, AvoEnv, AvoVerificationError, LoginSuccessEvent } from './Avo';
import codegenConfig from './AvoConfig';
 
const avo = Avo.init({ env: AvoEnv.Dev }, codegenConfig);
 
try {
  const payloads = avo.track(new LoginSuccessEvent(1730000000, 'team_42', 'acme.example'));
 
  const custom = payloads['Custom'];
  if (custom) {
    myAnalyticsSDK.logEvent(custom.name, custom.properties);
  }
} catch (e) {
  if (e instanceof AvoVerificationError) {
    console.error('[avo]', e.messages.join(', '));
  } else {
    throw e;
  }
}

What stays the same

  • Event names, property names, and validation rules. The tracking plan is unchanged — only the shape of the generated code differs.
  • Strict / noop modes. strict (throw on validation failure in development and staging) and noop (drop network calls) behave the same as in the single-source output — runtime validation is skipped entirely in production, and noop is force-disabled in production.
  • Inspector integration. Construct your Avo Inspector instance from the SDK and pass it into the init call — same as the single-source output.
  • Implementation status tracking. The CLI reports event-usage status the same way regardless of mode.
  • Destination interface. TypeScript’s AvoCustomDestination has the same shape as the single-source custom destination (make, logEvent, setUserProperties, identify, unidentify, logPage, revenue) — an existing single-source implementation is drop-in compatible.
  • The avo pull workflow. Same command, same authentication, same source/destination configuration in your Avo workspace.
  • Destination API keys. Still embedded by codegen. In TypeScript they’re in AvoConfig.ts; in Swift/Kotlin they’re inside AvoTrackingPlanConfig and used by the initAvo extension.

What changes

  1. Avo constructor signature. The single-source Avo(env:, customDestination:, ...) (Swift/Kotlin) and the free-function Avo.initAvo(config, destinations) (TypeScript) become Avo.initAvo(env:) (Swift/Kotlin) and Avo.init(config, codegenConfig) (TypeScript).
  2. Event method calls become event constructors and a single process / track call. Each avo.someEvent(...) becomes avo.process(SomeEvent(...)) (Swift/Kotlin) or avo.track(new SomeEvent(...)) (TypeScript).
  3. process() / track() throws AvoVerificationError. In development and staging, library mode in all three languages throws a typed AvoVerificationError when strict is on — wrap calls in try / do-catch, see the quick-start examples. In production, runtime validation is skipped entirely, so the call never throws.
  4. TypeScript: how custom destinations are wired. Custom destinations are passed in the destinations map of the runtime config, keyed by DestinationKey, instead of as separate Avo constructor arguments. All-or-nothing — provide an implementation for every DestinationKey, or omit destinations entirely and fan out manually. The AvoCustomDestination interface itself has the same shape as the single-source custom destination.
  5. System properties. setSystemProperties(...) / Avo.setSystemProperties(...) becomes AppSystemProperties.configure(...) (Kotlin) or AppSystemProperties.shared.configure({...}) (Swift, TypeScript). verify() and process() / track() throw AvoVerificationError with the message “AppSystemProperties.configure() must be called before sending events.” if a tracked event needs system properties and configure() hasn’t run.
  6. Imports. Add the library file to your build and import its types where you previously imported from the single file. In Swift this is import Library (if you packaged it that way); in Kotlin it’s import sh.avo.* (or whatever package your source path resolves to); in TypeScript it’s import { ... } from './Avo' (the app file re-exports the public library surface).