Giving Unomi Intelligence

(I am assuming you already have Unomi up and running in your machine. If you don’t, please check Unomi out of the box)

Whenever you start sending Events to Unomi, it will probably just run them through the platform and do absolutely nothing.

You, looking at all the nothing Unomi has just done

But wait, that's expected. Unomi is customizable, so it is waiting for you to define what should be processed and how! It let's you customize its logic in a way that can be expanded to any business that needs a CDP.

"But how do I do this?", you might ask? No more need to wait, I am here to guide you through its rules and conditions.

Rock Lee, from Naruto, giving my nice guy attitude a thumbs up

Unomi works as the following:

  • You send a User Event update or Profile Update to Unomi through an API (aka events);
  • Unomi has a list of all implemented rules (which are a pair of condition, action) that it iterates through to see if any condition matches the event;
  • If there are rules with conditions satisfied by the event, Unomi runs their actions.

We will not cover sending events to Unomi. You can check articles for that here. Today we are here to talk about rules, conditions and actions.

Rules

A pair of a condition with an action. We will have one implemented by the end of this article, but it's interesting and good to know that there are already some rules implemented out of the box. The following are all defined in the Plugins module: evaluateProfileAge, evaluateProfileSegments, modifyAnyConsent, newSession, sessionAssigned, sessionPageReferrer, sessionReferrer, updateProperties.

As for evaluateProfileAge, well, it's just some rule that is already built-in and that we will use to explain these concepts further.

All rules are just json files inside the src/main/resources/cxs directory, that follows the following format:

{
"metadata" : {
"id": "_ed2tvbd9p_evaluateProfileAge.json",
"name": "Evaluate profile age",
"description" : "Evaluate age when a profile is modified",
"readOnly":true
},

"condition" : {
"type": "profileUpdatedEventCondition",
"parameterValues": {
}
},

"actions" : [
{
"type": "evaluateProfileAgeAction",
"parameterValues": {
}
}
]

}

This is the evaluateProfileAge rule. It has a metadata object, that is used to describe the rule, a condition object, linking it to the actual condition, and a list of actions that should be executed when the condition is satisfied.

Conditions

They are pretty self-explanatory, and they can range from the most basic to the most complex. There are a bunch of conditions already implemented: profilePropertyCondition, for example, that checks if the profile has a specific property, or if a specific property is equal to or less than a specific value. There's also booleanCondition, that lets you create more complex rules linking conditions with the concepts of "OR" and "AND".

For evaluateProfileAge, the condition that will run is profileUpdatedEventCondition, that is implemented as:

{
"metadata": {
"id": "profileUpdatedEventCondition",
"name": "profileUpdatedEventCondition",
"description": "",
"systemTags": [
"profileTags",
"event",
"condition",
"eventCondition"
],
"readOnly": true
},
"parentCondition": {
"type": "eventTypeCondition",
"parameterValues": {
"eventTypeId": "profileUpdated"
}
},

"parameters": [
]
}

Which is basically saying that it does whatever its parent, eventTypeCondition, does, sending a parameter eventTypeId with the value "profileUpdated". eventTypeCondition just checks if the event has this specific property with this exact same value, as shown below:

{
"metadata": {
"id": "eventTypeCondition",
"name": "eventTypeCondition",
"description": "",
"systemTags": [
"profileTags",
"event",
"condition",
"eventCondition"
],
"readOnly": true
},
"parentCondition": {
"type": "eventPropertyCondition",
"parameterValues": {
"propertyName": "eventType",
"propertyValue": "parameter::eventTypeId",
"comparisonOperator": "equals"
}
},
"parameters": [
{
"id": "eventTypeId",
"type": "String",
"multivalued": false
}
]
}

See the parameters? It's comparing if the event eventTypeId equals the Id of the received event.

If you don't want to just use the already implemented conditions and want to create your own, that's also possible. Let's take booleanCondition as our example:

{
"metadata": {
"id": "booleanCondition",
"name": "booleanCondition",
"description": "",
"systemTags": [
"profileTags",
"logical",
"condition",
"profileCondition",
"eventCondition",
"sessionCondition",
"sourceEventCondition"
],
"readOnly": true
},
"conditionEvaluator": "booleanConditionEvaluator",
"queryBuilder": "booleanConditionESQueryBuilder",
"parameters": [
{
"id": "operator",
"type": "String",
"multivalued": false,
"defaultValue": "and"
},
{
"id": "subConditions",
"type": "Condition",
"multivalued": true
}
]
}

booleanCondition defines which parameters it receives, and the name of the bean that will actually evaluate the condition, booleanConditionEvaluator. It is defined in src/main/java/org/apache/unomiplugins/baseplugin/BooleanConditionEvaluator.java.

public class BooleanConditionEvaluator implements ConditionEvaluator {

@Override
public boolean eval(Condition condition, Item item, Map<String, Object> context,
ConditionEvaluatorDispatcher dispatcher) {
boolean isAnd = "and".equalsIgnoreCase((String) condition.getParameter("operator"));
@SuppressWarnings("unchecked")
List<Condition> conditions = (List<Condition>) condition.getParameter("subConditions");
for (Condition sub : conditions) {
boolean eval = dispatcher.eval(sub, item, context);
if (!eval && isAnd) {
// And
return false;
} else if (eval && !isAnd) {
// Or
return true;
}
}
return isAnd;
}
}

If you want to create your own Condition, all you have to do is create a class that implements the ConditionEvaluator object, overriding the eval function.

As mentioned above, the booleanCondition is just a way for us to link subconditions with the concepts of "OR" and "AND", and that's exactly what the code above shows us.

Actions

Last, but not least, we have actions. Actions is what is going to happen if a condition is satisfied, in a rule. In our evaluateProfileAge example, the defined action is called evaluateProfileAgeAction.

By now you should have guessed that we have another json file for it, right?

{
"metadata": {
"id": "evaluateProfileAgeAction",
"name": "evaluateProfileAgeAction",
"description": "",
"systemTags": [
"profileTags",
"event"
],
"readOnly": true
},
"actionExecutor": "evaluateProfileAge",
"parameters": [
]
}

That pretty much defines its actionExecutor:

public class EvaluateProfileAgeAction implements ActionExecutor {

@Override
public int execute(Action action, Event event) {
boolean updated = false;
if (event.getProfile().getProperty("birthDate") != null) {
Integer y = Years.yearsBetween(new DateTime(event.getProfile().getProperty("birthDate")), new DateTime()).getYears();
if (event.getProfile().getProperty("age") == null || event.getProfile().getProperty("age") != y) {
updated = true;
event.getProfile().setProperty("age", y);
}
}
return updated ? EventService.PROFILE_UPDATED : EventService.NO_CHANGE;
}
}

That means that actions are just classes that implement the ActionExecutor interface, overriding its execute method. For our EvaluateProfileAgeAction, it checks whether the profile at hand — the profile that triggered the user event — has a birthDate and, if it has, checks whether age is already defined. If it's not, it will define it for us.

Featuring: Segments

Now you are able to create Rules, Actions and Conditions that will let you modify profile information. There's a lot you can do with it, but the main reason you are doing all that is so you can segment those profiles, right?

Segments are really close to rules, with the main difference being that they don't have actions. You define a condition for them. If the condition is satisfied, the profile will automatically be a part of that specific segment.

{
"metadata": {
"id": "heavyUser",
"name": "heavyUser",
"scope": "systemscope",
"description": "The user has this segment if it has acessed our products Website.",
"readOnly":true
},
"condition": {
"parameterValues": {
"subConditions": [
{
"parameterValues": {
"propertyName": "properties.visitedSiteNb",
"comparisonOperator": "greaterThan",
"propertyValueInteger": "5"
},
"type": "profilePropertyCondition"
}
],
"operator" : "and"
},
"type": "booleanCondition"
}
}

The segment heavyUser is given to a profile that has accessed our product's website more than five times, i.e., a profile that has the property visitedSiteNb greater than 5.

Whenever an action is run because of a profile update or user event, and the action changes something within the profile, the segments update for that profile will run.

Patching things up

Everything is patched up following Apache Blueprint definitions. Below, there is a simple blueprint used in Unomi's documentation example:

<blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">

<reference id="profileService" interface="org.apache.unomi.api.services.ProfileService"/>

<!-- Action executor -->
<service id="incrementTweetNumberAction" interface="org.apache.unomi.api.actions.ActionExecutor">
<service-properties>
<entry key="actionExecutorId" value="incrementTweetNumber"/>
</service-properties>
<bean class="org.apache.unomi.samples.tweet_button_plugin.actions.IncrementTweetNumberAction">
<property name="profileService" ref="profileService"/>
</bean>
</service>
</blueprint>

Where you define the IncrementTweetNumberAction as a service and give it the actionExecutorId of incrementTweetNumber.

Once you created a project with your own rules, conditions and actions and patched things using blueprint, to use them in your Unomi, you just have to generate a .jar for your project and put it in Unomi's /deploy folder. Since Unomi uses karaf, you can hot deploy new things in runtime.