Skip to main content

Developing Plugins

The Java API allows you to develop a plugin using arbitrary Java code. We do not support Ruby for generating plugins, so if you are using Ruby, skip to the next section.

Setting Up Your Project

The Causal examples repository has an example gradle file that can be used as a model when setting up your build script to develop plugins. It covers how to:

  1. Use the Causal compiler to generate the code you will need as a base to write your plugins
  2. Package up your code into a jar that can be run inside the impression server
  3. Launch an impression server locally and attach a debugger
  4. Use the Java API in your test code to make the impression server invoke your plugin

Attaching a Plugin

The first step in defining a plugin is to declare it in the FDL file. Plugins can be added to session, feature, and event sections in FDL. Session plugins are run when a new session starts. Feature plugins are run every time you generate a feature impression. Event plugins are run each time an event fires.

Define a session plugin using the plugin keyword inside the session block:

session {
args {
visitorId : ID! @persistent_key
arrivalId : ID! @session_key
userId : ID @mutable
}
plugin java "SessionPlugin" {
userZipCode : String! = ""
}
event Click {
clickValue : Int!
}
}

The plugin section declares the Java class that will be called when a new session is created, and the set of output values that the plugin can write into the session object.

For this example, you'll need to define a class called SessionPlugin in the package declared by the FDL namespace keyword. The Causal compiler will generate an interface for you named SessionPluginBase that your new class should implement. You may also specify the full name of your class (including the package name) in the plugin but the base class will still stay in the namespace defined package.

Fill method

The fill method is called when the session is first created. It is your responsibility to fill in the plugin outputs during this call. To do so, you may read any values from the session context, compute anything you like (including calling out to other data systems), and write the resulting values to the plugin's fields. You may also call out to external data systems to notify them of the new session.

For this example:

import java.util.UUID;

public class SessionPlugin implements SessionPluginBase {
public SessionPlugin() {
System.out.println("Started");
}

@Override
public void fill(Session session) throws Exception {
String visitorId = session.getVisitorId();
// look up a bunch of stuff here
session.setUserZipCode("02445");

Variant Selection

Session plugins also have an optional newVariant method that is called when the impression server selects a new experiment variant for a session. You can override this method in order to log variant selections to external systems, or do any other required processing. The method signature is:

  }

@Override
public void newVariant(
Session session,
String experimentName,

You have access to the experiment, the variant, and any feature attribute settings specified by the variant.

newVariant is called in a background thread, so it will not block other requests to the impression server. There is no need to process these inside your own thread pool.

The method may be called concurrently by several different threads in the impression server (for different experiments and sessions). If your code is not thread-safe, guard it with synchronized.

Feature Plugins

Feature plugins are defined the same way, except the base class makes both the session and the feature available to the fill method. Again, inside the fill method it is your responsibility to fill in the plugin outputs. However, you should not use fill to pass impression information to an external data system. Use the onImpression method below for that.

You declare a feature plugin the same way you declare a session plugin, using the plugin keyword.

feature Simple
{
args {
simpleInput : Int!
}
plugin java "SimplePlugin" {
"""This value is set by the io.causallabs.example.SimplePlugin class.
If the class doesn't fill in the value, the default of '0' will be used. """
simplePluginOutput : Int! = 0
}
output {
simpleOutput : Int = 42 @external( values: ["BigNumber", "SmallNumber"] )
}
event Click {

All the session's values are available, not just the context. So, you can use session's plugin values in order to compute the feature's plugin values.

package io.causallabs.example;

public class SimplePlugin implements SimplePluginBase {

@Override
public void fill(Session session, Simple simple) throws Exception {
// use the eval method to populate the plugin output values
simple.setSimpleOutput(simple.getSimpleInput() + 1);
}

OnImpression method

Unlike session plugins, you should not call out to external data systems to notify them of the new feature impression inside the fill method. The fill method is only called when the plugin outputs need to be populated. That means it does not get called exactly when a feature impression occurs in these two situations:

  1. When you are filling the cache on the server for server side rendering.
  2. When you are serving a memoized impression, and therefore do not need to recompute the outputs.

If you need a callback each time an impression occurs, use the onImpression method instead.

  public void fill(Session session, Simple simple) throws Exception {
// use the eval method to populate the plugin output values
simple.setSimpleOutput(simple.getSimpleInput() + 1);
}

@Override
public void onImpression(Session session, Simple simple) throws Exception {
// use the register method to tell other data systems that an impression has occured
// all values are available because you are guaranteed that eval has previously been called
System.out.println(
"Simple impression input:"
+ simple.getSimpleInput()
+ " plugin:"
+ simple.getSimplePluginOutput()
+ " output:"

Event Plugins

Event plugins are defined the same way, just inside an event definition.

feature Simple
{
args {
simpleInput : Int!
}
plugin java "SimplePlugin" {
"""This value is set by the io.causallabs.example.SimplePlugin class.
If the class doesn't fill in the value, the default of '0' will be used. """
simplePluginOutput : Int! = 0
}
output {
simpleOutput : Int = 42 @external( values: ["BigNumber", "SmallNumber"] )
}
event Click {
clickValue : Int!
plugin java "SimpleClickPlugin" {}
}
}

Typically event plugins are used to augment the contents of the event with data grabbed from backend systems. Another common use is to forward the event to another data system for further processing.

The onEvent method of an event plugin inside a feature definition has access to the session, feature impression, and event data. Inside a session or abstract event definition, you only get access to the session and event data.

You may read any of theses values, do any processing, and write values to the plugin outputs. Any plugins defined on imported events will also get run before any plugin on the importing event.

package io.causallabs.example;

import io.causallabs.example.Simple.Click;

public class SimpleClickPlugin implements SimpleClickPluginBase {

@Override
public void onEvent(Session session, Simple impression, Click click) throws Exception {
System.out.println("Simple click with value: " + click.getClickValue());
}
}

Disabling Logging

You may use a plugin to disable logging for a session. This can be useful if you have identified a session as internal testing traffic or a bot. Use the Session.setNoLog(boolean) method from any plugin call to disable logging.

When set true, the session will not be persisted into the data warehouse tables, nor will it count towards any metrics.

Closing a Plugin

Java plugins implement AutoCloseable. The close method is called after the impression server is sure there will be no more api calls to the plugin object. You can use the close method to flush any buffers or do any other cleanup that is required before the plugin is unloaded.

Generic API

In addition to defining plugins for individual features and events, you may use the generic interface, which gets called for every feature impression and event. The below examples are exactly equivalent to the methods from the previous sections.

To populate plugin outputs using this API, override the generic fill method on your session plugin.

      UUID experimentId,
String variantName,
UUID variantId,
List<VariantValue> values) {
System.out.println("Chose variant " + variantName + " for experiment " + experimentName);
}

As you can see, it is up to you to check the type of the arguments and do appropriate casting with this approach. However, it may be cleaner if you'd like to do a similar operation with a lot of different types of impressions.

The impression object can be cast to the Java interface generated by the Causal compiler for the feature. The interface inherits the name of the feature from the FDL file.

To send impressions to a third party data system, override the generic onImpression session plugin method:

    if (impression instanceof Simple) {
Simple simple = (Simple) impression;
// use the eval method to populate the plugin output values
simple.setSimplePluginOutput(simple.getSimpleInput() + 1);
}
}

@Override
public void onImpression(Session session, Impression impression) throws Exception {
if (impression instanceof Simple) {
Simple simple = (Simple) impression;
// use the register method to tell other data systems that an impression has occured

To capture all events, override the generic onEvent method, also on the session plugin.

      // all values are available because you are guaranteed that eval has previously been
// called
System.out.println(
"Simple impression input:"
+ simple.getSimpleInput()
+ " plugin:"
+ simple.getSimplePluginOutput()

Again, it is up to you to do the casting. Java event interfaces are named the same as the event in the FDL file. When an event is defined inside a feature, it's Java event interface is FeatureName.EventName.

Generic API outputs

You may define output values for the generic interface also. Simply drop the java keyword.

feature SimpleGeneric
{
plugin {
"This output can only be set by the Generic plugin methods"
anotherOutput : Int! = 0
}
}

This plugin block will add a plugin output field on the SimpleGeneric feature. However, since there is no code declared to set the output, it will stay set to it's default value of 0 unless it is changed during a call to newImpression.

You cannot currently define both a generic plugin block and a java plugin block in the same place.

Running the Plugin

In order to run the plugin inside the impression server, you must package it up in a jar and include all the required dependencies.

The impression server takes a --plugin command line argument that can be used to load this jar. You can either mount a volume inside your docker image that includes your jar, or build a new image using ours as a base that includes your jar.

An example dockerfile for the latter may be:

FROM causallabs/iserver:0.47.0
COPY build/libs/my-cool-jar.jar /root/jars/
WORKDIR /root
ENTRYPOINT ["./bin/iserver", "--plugin", "./jars/my-cool-jar.jar"]

You may specify multiple plugin paths using several --plugin options. They are searched in the order that you specify. If a path ends in '/' it is assumed to be a directory, otherwise a jar.

If you have a lot of dependencies in your plugin jar, you may want to consider constructing a single "fat" jar that includes everything.