Type safe code, unit tests and CI
Avo Codegen technical deep dive

Avo Codegen technical deep dive

Avo generated file with event tracking functions

The Avo generated code is the representation of the tracking plan you define in Avo for a given source. You can think of a source as a platform that sends events. For example if you set up an iOS/Swift source the generated code will be in a .swift file. Since the tracking plans are different for each customer the content of this file is also different. We generate the files and you can download them either with the Avo CLI or in the Avo web interface in Codegen tab.

First of all, feel free to explore your generated file. It's deliberately designed to be easily readable. Our code generation is under active development and some advanced or new features may slightly differ from one platform to another. The generated file is the best source of truth when looking for implementation details.

You should never have to make changes to the Avo generated file. If you find yourself in the situation where you have to edit the file please contact us.

Make sure all the events you want to implement using Codegen have the source attached to it, where "Implement with Codegen" has been checked. Learn more about how to configure events in your tracking plan here.

Avo Command Line Interface

The Avo CLI is the easiest way to interact with Avo from your development environment. Run

npm install -g avo

to install the CLI. Then login with

avo login

And pull the Avo Generated file with the following command

avo pull [--branch my-branch] [SourceName]

it will guide you through the initial setup and will create a config file called avo.json in the directory. Next time when you pull the setup will be picked from that file.

Make sure to check the avo.json file in to your source control to keep the Avo configuration is in sync across your team.

Learn more about the CLI here.

Initialization

First thing you need to do after importing the generated file is to initialize Avo. There will either be a static initAvo method or a class, named Avo by default, with a constructor, depending on your setup. Contact us if you want to change the setup. Regardless of the option, the arguments you would need to provide are the same, and based on your tracking plan. Check out the Codegen tab for specific recommendations. Samples below are in Swift and the code is very similar in other programming languages:

public static func initAvo(env: AvoEnv,
    requiredStringSystemProperty: RequiredStringSystemProperty,
    optionalStringSystemProperty: OptionalStringSystemProperty?,
    requiredIntSystemProperty: Int,
    optionalFloatSystemProperty: Double?,
    optionalListSystemProperty: [Int]?,
    firstDestination: AvoCustomDestination,
    secondDestination: AvoCustomDestination,
    debugger: NSObject, strict: Bool = false, noop: Bool = false)
  • env - Avo is designed to act differently based on the environment. Supported values are development, staging and production. Most important differences include sending data to different projects in your analytics (to not pollute production data with dev/testing session) and ability to crash the app in development if Avo detects an error in the analytics.
  • system properties - this are properties designed to be sent with each event. You need to provide them during initialization. You can update their values later with the setSystemProperties method.
public static func setSystemProperties(
    requiredStringSystemProperty: RequiredStringSystemProperty,
    optionalStringSystemProperty: OptionalStringSystemProperty?,
    requiredIntSystemProperty: Int,
    optionalFloatSystemProperty: Double?,
    optionalListSystemProperty: [Int]?);

Read more about property types later in the Codegen section.

  • firstDestination and secondDestination - each destination from the source/destination connections you've defined in the Avo dashboard is represented as its own Destination Interface in the init call. This is how Codegen routes data, locally in your client (without the data ever going through Avo's servers), to the correct destination based on the source/destination connections you've defined in the Avo dashboard.
  • debugger - mobile platforms (Android, iOS, React Native) support the Avo Analytics Debuggers, provide one here if you want to connect it to Avo events. You don't need to provide the debugger on the web to use it there.
  • strict - if this is set to true Avo would crash your app in the development environment when it sees errors. Note that the Avo validation will never crash your app if env is set to Production.
  • noop - if this flag is set to true Avo won't make any network calls (no tracking) in development and staging environments. Note that the noop flag is dismissed in production.
  • destination options (not shown in the example) - some platforms, for example JavaScript, allow you to provide destination options for the analytics destinations that will be passed during tracking SDK initialization. This allows you to pass config/options through Avo to the init functions of the underlying analytics SDKs.
  • avoLogger (not shown in the example) - custom logger implementation to control logs logic. Can be used to disable logs. More about custom loggers.
  • inspector (not shown in the example) - instance of Avo Inspector to automatically send Codegen data to Avo Inspector (on the supported platforms)

What's inside the initAvo method / Avo constructor?

There are a few things happening, like setting the flags, system properties and reporting implementation status in development mode to the Avo servers, so you can check whether the event has ever been successfully invoked in development, on the Avo website.

But the most important part is initialization of the analytics destinations. Depending on the setup of your tracking plan you might see something like

if __ENV__ == .prod {
    amplitudeDestination = AmplitudeDestination()
    amplitudeDestination?.make("production Amplitude key is automatically here based on the data you provided in the tracking plan"")
    mixpanelDestination = MixpanelDestination()
    mixpanelDestination?.make("production Mixpanel key is automatically here based on the data you provided in the tracking plan"")
}
if __ENV__ == .dev {
    amplitudeDestination = AmplitudeDestination()
    amplitudeDestination?.make("development Amplitude key is automatically here based on the data you provided in the tracking plan"")
    mixpanelDestination = MixpanelDestination()
    mixpanelDestination?.make("development Mixpanel key is automatically here based on the data you provided in the tracking plan"")
}

The keys are set up in the Avo web UI and automatically fetched by Codegen, so you don't need to do anything here, just know that it is there. The API keys in Avo are version controlled, and synced across all your sources, this way you don't need to worry if correct API key is being used or not, Avo takes care of it for you.

Codegen

Codegen produces actual callable functions generated for events defined in your tracking plan.

On some server platforms Codegen produces functions which are async, e.g. on Node.js they return a Promise and on C# you can await on the function

For example if you have an event defined with name App Opened connected to your source, Codegen interface will look like

/**
    App Opened: This event is sent immediately after the user opens the app [This description is defined in the Tracking Plan on avo.app]
 
    - SeeAlso: [App Opened](https://www.avo.app/link-to-the-event-in-your-tracking-plan)
*/
public func App Opened(
    beverage: Beverage,
    userId: String?) {...}

Supported tracking methods (Avo Actions)

Each event in your tracking plan added to Codegen creates an event function that is a code generated wrapper that prepares and routes the event tracking data to your analytics SDK tracking libraries. Each event function can wrap multiple tracking methods, such as logging events, identifying users, updating user properties, etc. The default tracking method is to log an event. To configure which tracking methods each Avo Function wraps, you change or add "Action Types" on the event in your Avo Tracking Plan.

đź“–

Property types

At this point you would notice that the parameters can have various types.

There are a few dimensions in which the parameters can differ:

Type

Avo supports the following types: string, int, float, bool and object.

Single value / list

You would be required to provide a list of things if the property is marked as a list.

Required / Optional

In Avo we made a design decision that you always need to provide a value of a property, even if it is optional to mitigate the risk of accidentally not providing the value and damaging the data. If the property is marked as optional you would be able to provide null as it's value in the cases when the property should not be sent.

Value Constraints

You can notice that in the example above the first argument is not a String, but some other type. It happens because there is a constraint specified in the tracking plan that limits the possible values of this parameter. Avo has generated an enum for you:

public enum Beverage : String {
    case beer = "beer"
    case wine = "wine"
    case cocktail = "cocktail"
    case water = "water"
}

The second parameter has no constraints, so it is a String (though it is optional, because it's marked as optional in the tracking plan).

In general, constraints can be added to properties to validate against:

string properties: a finite list of allowed string values int and float properties: a minimum and maximum value object properties: exact structure of the allowed keys and types of values in the object

Avo can add helper parameters that are not defined in the tracking plan, but are known to be required to make that specific call, for example in server environments Avo usually adds a userId parameter to each function, so server can provide user id to the analytics tracking services.

What's happening inside Codegen?

Avo makes sure that the event names and provided properties are correct according to the tracking plan with compile time and runtime validations and development invocations are reported to Avo servers so you can check the invocation status on the Avo website.

Then it sends the provided data to all the configured analytics destinations, using the destination interfaces you've defined during initialization. There are different actions that can be tracked: identify, log event, update user properties, unidentify, log revenue, log page view, but everything is checked and the payload is prepared inside the Avo file and delivered in a correct form to the destination interfaces.

The Avo generated tracking code will look similar to this:

// first destination
var firstDestinationEventProperties: [String: Any] = [:]
// firstDestinationEventProperties will be populated with all the provided properties to this Avo Function and all the system properties.
firstDestination.logEvent(eventName: "Logged Out", eventProperties: firstDestinationEventProperties)
secondDestination.unidentify()
// second destination
var secondDestinationEventProperties: [String: Any] = [:]
// secondDestinationEventProperties will be populated with all the provided properties to this Avo Function and all the system properties.
secondDestination.logEvent(eventName: "Logged Out", eventProperties: customDestEventProperties)
secondDestination.unidentify()

This particular event has log event and unidentify actions. The methods called above will vary based on the actions of the given event. Read more about actions here.

Data Validations

Avo helps developers avoid mistakes when implementing analytics, it makes sure that your code does exactly what it is supposed to do based on the tracking plan defined in Avo, in two ways.

First, Avo is utilizing the type system of a programming language you work with. For example in the code snippet above the optional properties are supported on the language level in Swift, so we make the interface in a way you can't provide null values where they are not expected. We try to do as many validations as possible this way, so they are performed in the compile time.

Second, it's runtime validations. In cases when the language does not provide us with tool to check in the compile time, like in JavaScript, we do runtime checks, looking like this

assertString: function assertString(propertyId, propName, str) {
  if (typeof str !== 'string') {
    var message =
      propName +
      ' should be of type string but you provided type ' +
      typeof str +
      ' with value ' +
      JSON.stringify(str);
    return [
      {
        tag: 'expectedStringType',
        propertyId: propertyId,
        message: message,
        actualType: typeof str,
      },
    ];
  } else {
    return [];
  }
}

A check like this is run when the Avo function is triggered. Then the message is delivered to the logs, debugger or as a crash in the strict mode depending on your setup.