Tracking interests with a custom Groovy Action

November 14, 2023

Overview

Continuing from the events we’ve been collecting using the previous tutorial, we’re going to configure jCustomer to perform various types of analysis.

In this tutorial we will be collecting interests (Healthcare or others), and automatically add them to user profiles, the objective being to obtain an array of interests such as:

{
  "interests": [
    { "healthcare": 5 },
    { "automotive": 1 },
    { "sports": 10 },
    { "cooking": 3 },
    { "technology": 2 }    
  ]
}

Collect interests

As explained above, the objective of our first step is to collect interests and append them to a profile, if this profile already has the interest, then its count gets incremented.

In jCustomer, such an operation require the user a two elements:

  • A rule must be created to act when an event condition is met.
  • An action will be called by the rule on all events matching its condition.

Introducing Karaf shell

Before proceeding further with creating actions and rules, it is important to get familiar with karaf tools and in particular event-tail and rule-tail which are a useful mean of verifying various operations.

The Karaf shell is accessible via SSH, by default on port 8012.

ubuntu@ip-10-0-3-253:~$ ssh -p 8102 karaf@localhost
Password authentication
Password:
        __ __                  ____
       / //_/____ __________ _/ __/
      / ,<  / __ `/ ___/ __ `/ /_
     / /| |/ /_/ / /  / /_/ / __/
    /_/ |_|\__,_/_/   \__,_/_/

  Apache Karaf (4.2.11)

Hit '<tab>' for a list of available commands
and '[cmd] --help' for help on a specific command.
Hit 'system:shutdown' to shutdown Karaf.
Hit '<ctrl-d>' or type 'logout' to disconnect shell from current session.

karaf@root()>

The event-tail command will automatically list events as they’re being received by the system, for example if you were to click on “Healthcare” again.

karaf@root()> event-tail
ID         |Type    |Session      |Profile        |Timestamp                    |Scope          |Persi|
-------------------------------------------------------------------------------------------------------
34b[...]0f7|click   |a37[...]566  |bd3[...]304    |Mon Nov 29 18:25:58 UTC 2021 |digitall       |true |

Similarly, the “rule-tail” will list rules as they’re being executed by the system and can be used as an easy way to validate that rule conditions are properly defined.

Register an action

We will begin by registering a new Groovy Action in jCustomer, for now we only want to make sure our action actually gets triggered when an event is received, so we’re going to keep its content to a minimum (displaying a log).

Save the code below to a file called “interestAction.groovy”
 

import java.util.logging.Logger

@Action(id = "scriptGroovyAction",
        description = "A Groovy action recording interests",
        actionExecutor = "groovy:interestAction",
        hidden = false)
def execute() {
    Logger logger = Logger.getLogger("scriptGroovyAction")
    logger.info("Groovy action for event type: " + event.getEventType())
    EventService.NO_CHANGE
}

From the same folder, we’re going to use curl to submit the action to jCustomer (don't forget to update credentials as needed).

curl --request POST \
  --url http://localhost:8181/cxs/groovyActions \
  --user karaf:karaf \
  --form file=@interestAction.groovy

We will revisit the action once we confirmed that it gets properly triggered when an event is received.

Register a rule

Now that we have an action that will be doing something, we can create a rule to trigger it on events matching a particular condition. Luckily for us, we defined that condition earlier when we were using unomi to search for our events.

Using curl, we’re going to submit the following rule:

curl --request POST \
  --url http://localhost:8181/cxs/rules \
  --user karaf:karaf \
  --header 'Content-Type: application/json' \
  --data '{
  "metadata": {
    "id": "testGroovyActionRule",
    "name": "Test Groovy Action Rule",
    "description": "A sample rule to test Groovy actions"
  },
  "condition": {
    "type": "eventTypeCondition",
    "parameterValues" : {
       "eventTypeId" : "click"
    }
  },
  "actions": [
    {
      "type": "scriptGroovyAction",
      "parameterValues": {}
    }
  ]
}'

Validate rule execution

If all is going well, submitting a new event by clicking on “Healthcare” on Digitall should trigger the rule.

Open-up the Karaf shell and run the “rule-tail” command.

karaf@root()> rule-tail
Rule ID              |Rule Name       |Event Type |Session    |Profile    |Timestamp      |Scope|
-------------------------------------------------------------------------------------------------
testGroovyActionRule |Test [...] Rule |click      |a02[...]b5c|bd3[...]c89|Mon Nov 29 20:1|digit|

If you look into jCustomer logs you should also see the log message we added to the action.

Nov 29 20:15:23 ip-10-0-3-140 docker_jcustomer[9776]: 2021-11-29T20:15:23,624 | INFO  | qtp1054571393-286 |   | 5 - org.ops4j.pax.logging.pax-logging-api - 1.11.9 |  Groovy action for itemID: healthcare

Update the action

By following the tutorial up to that point we managed to trigger our action on a particular type of event, our next step will consist in updating the action to populate the interests array.

Log messages are present in this example to facilitate debugging, you should be cautious when using logs in production.
\

import java.util.logging.Logger;

import org.apache.unomi.api.Event;
import org.apache.unomi.api.Profile;
import org.apache.unomi.api.services.EventService;

@Action(id = "scriptGroovyAction",
        description = "A Groovy action recording interests",
        actionExecutor = "groovy:interestAction",
        hidden = false
        )

def execute() {
    Logger logger = Logger.getLogger("groovyActionInterests")

    final String EVENT_INTERESTS_PROPERTY = "interests";
    final Profile profile = event.getProfile();

    Map<String, Double> srcInterestsMap = (Map<String, Double>) profile.getProperty( EVENT_INTERESTS_PROPERTY );

    final Map<String, Double> dstInterestsMap = new HashMap<>();

    if ( srcInterestsMap == null || !srcInterestsMap.containsKey(event.target.itemId)) {
        // The profile does not have any interests or the received interest
        // does does not exist yet in the profile
        dstInterestsMap.putAll(srcInterestsMap)
        dstInterestsMap.put(event.target.itemId, 1)
    } else {
        // Copying the interests and incrementing the one which was just received
        srcInterestsMap.forEach((interest, clickCount) -> {
            if (interest == event.target.itemId) {
                dstInterestsMap.put(interest, clickCount + 1)
            } else {
                dstInterestsMap.put(interest, clickCount)
            }
        });
    }

    logger.info("Profile interests before processing the event: " + srcInterestsMap)
    logger.info("Profile interests after processing the event: " + dstInterestsMap)

    // Save the updated interests property
    event.getProfile().setProperty(EVENT_INTERESTS_PROPERTY, dstInterestsMap)

    return EventService.PROFILE_UPDATED;   
}

Triggering an event will give you the following logs:
 

2021-11-30T04:19:56,261 | INFO  | qtp1407221069-237 | | 5 - org.ops4j.pax.logging.pax-logging-api - 1.11.9 |  Profile interests before processing the event: [media:5, healthcare:17]
2021-11-30T04:19:56,262 | INFO  | qtp1407221069-237 | | 5 - org.ops4j.pax.logging.pax-logging-api - 1.11.9 |  Profile interests after processing the event: [media:5, healthcare:18]

As you can see, interests for the user who clicked on the site are increasing according to the element being clicked on.

Conclusion

In this tutorial, we reviewed how a Groovy action can be created and enabled on jCustomer. We used interest-tracking as an example, but Apache Unomi already has a built-in action to track interests, action we would recommend using in production.