Skip to content

Commit

Permalink
Issue 30382 analytics user event rules actionlet (#30415)
Browse files Browse the repository at this point in the history
Adding the changes to allow to fire an event on a rule
  • Loading branch information
jdotcms authored Oct 23, 2024
1 parent 1da9702 commit b5017f1
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import com.dotmarketing.portlets.rules.actionlet.PersonaActionlet;
import com.dotmarketing.portlets.rules.actionlet.RuleActionlet;
import com.dotmarketing.portlets.rules.actionlet.RuleActionletOSGIService;
import com.dotmarketing.portlets.rules.actionlet.RuleAnalyticsFireUserEventActionlet;
import com.dotmarketing.portlets.rules.actionlet.SendRedirectActionlet;
import com.dotmarketing.portlets.rules.actionlet.SetRequestAttributeActionlet;
import com.dotmarketing.portlets.rules.actionlet.SetResponseHeaderActionlet;
Expand Down Expand Up @@ -195,6 +196,7 @@ public class RulesAPIImpl implements RulesAPI {
.add(VisitorTagsActionlet.class)
.add(SendRedirectActionlet.class)
.add(StopProcessingActionlet.class)
.add(RuleAnalyticsFireUserEventActionlet.class)
.build();

public RulesAPIImpl() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.dotcms.analytics.track.collectors;

import com.dotcms.analytics.track.matchers.UserCustomDefinedRequestMatcher;
import com.dotmarketing.beans.Host;

import java.util.HashMap;
import java.util.Objects;

/**
* This event collector creator basically allows to send a message for a customer event.
* These events are fired by rest, wf and rules.
* @author jsanca
*/
public class CustomerEventCollector implements Collector {
@Override
public boolean test(final CollectorContextMap collectorContextMap) {

return UserCustomDefinedRequestMatcher.USER_CUSTOM_EVENT_MATCHER_ID.equals(collectorContextMap.getRequestMatcher().getId()) ; // should compare with the id
}

@Override
public CollectorPayloadBean collect(final CollectorContextMap collectorContextMap,
final CollectorPayloadBean collectorPayloadBean) {
final String uri = (String)collectorContextMap.get("uri");
final String host = (String)collectorContextMap.get("host");
final Host site = (Host) collectorContextMap.get("currentHost");
final String language = (String)collectorContextMap.get("lang");
collectorPayloadBean.put("url", uri);
collectorPayloadBean.put("host", host);
collectorPayloadBean.put("language", language);
collectorPayloadBean.put("site", null != site?site.getIdentifier():"unknown");
final String eventType = (String)collectorContextMap.get("eventType") == null?
EventType.CUSTOM_USER_EVENT.getType():(String)collectorContextMap.get("eventType");
collectorPayloadBean.put("event_type", eventType);

return collectorPayloadBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum EventType {
VANITY_REQUEST("VANITY_REQUEST"),
FILE_REQUEST("FILE_REQUEST"),
PAGE_REQUEST("PAGE_REQUEST"),
CUSTOM_USER_EVENT("CUSTOM_USER_EVENT"),

URL_MAP("URL_MAP");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ private static class WebEventsCollectorServiceImpl implements WebEventsCollector
WebEventsCollectorServiceImpl () {

addCollector(new BasicProfileCollector(), new FilesCollector(), new PagesCollector(),
new PageDetailCollector(), new SyncVanitiesCollector(), new AsyncVanitiesCollector());
new PageDetailCollector(), new SyncVanitiesCollector(), new AsyncVanitiesCollector(),
new CustomerEventCollector());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,7 @@ private void copySitePropertiesFromForm(final SiteForm siteForm, final Host site

if (UtilMethods.isSet(siteForm.getTagStorage())) {
final Host tagStorageSite =

Try.of(() -> this.siteHelper.getSite(APILocator.systemUser(), siteForm.getTagStorage())).getOrNull();
if (null == tagStorageSite) {
throw new IllegalArgumentException(String.format("Tag Storage Site '%s' was not found", siteForm.getTagStorage()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.dotmarketing.portlets.rules.actionlet;

import com.dotcms.analytics.track.collectors.WebEventsCollectorService;
import com.dotcms.analytics.track.collectors.WebEventsCollectorServiceFactory;
import com.dotcms.analytics.track.matchers.UserCustomDefinedRequestMatcher;
import com.dotcms.util.DotPreconditions;
import com.dotmarketing.portlets.rules.RuleComponentInstance;
import com.dotmarketing.portlets.rules.model.ParameterModel;
import com.dotmarketing.portlets.rules.parameter.ParameterDefinition;
import com.dotmarketing.portlets.rules.parameter.display.TextInput;
import com.dotmarketing.portlets.rules.parameter.type.TextType;
import com.dotmarketing.util.UUIDUtil;
import com.dotmarketing.util.WebKeys;
import com.liferay.util.StringPool;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* This actionlet allows to fire an user event to the analytics backend.
* @author jsanca
*/
public class RuleAnalyticsFireUserEventActionlet extends RuleActionlet<RuleAnalyticsFireUserEventActionlet.Instance> {

public static final UserCustomDefinedRequestMatcher USER_CUSTOM_DEFINED_REQUEST_MATCHER = new UserCustomDefinedRequestMatcher();
private final transient WebEventsCollectorService webEventsCollectorService;

private static final long serialVersionUID = 1L;
public static final String RULE_EVENT_TYPE = "eventType";
public static final String RULE_OBJECT_TYPE = "objectType";
public static final String RULE_OBJECT_ID = "objectId";
public static final String RULE_REQUEST_ID = "requestId";
public static final String RULE_CONTENT = "CONTENT";
public static final String RULE_ID = "id";
public static final String RULE_OBJECT_CONTENT_TYPE_VAR_NAME = "object_content_type_var_name";
public static final String RULE_OBJECT = "object";
public static final String RULE_EVENT_TYPE1 = "event_type";

public RuleAnalyticsFireUserEventActionlet() {
this(WebEventsCollectorServiceFactory.getInstance().getWebEventsCollectorService());
}

public RuleAnalyticsFireUserEventActionlet(final WebEventsCollectorService webEventsCollectorService) {
super("api.system.ruleengine.actionlet.analytics_user_event",
new ParameterDefinition<>(0, RULE_EVENT_TYPE, new TextInput<>(new TextType().required())),
new ParameterDefinition<>(1, RULE_OBJECT_TYPE, new TextInput<>(new TextType())),
new ParameterDefinition<>(2, RULE_OBJECT_ID, new TextInput<>(new TextType()))
);

this.webEventsCollectorService = webEventsCollectorService;
}

@Override
public Instance instanceFrom(Map<String, ParameterModel> parameters) {
return new Instance(parameters);
}

@Override
public boolean evaluate(final HttpServletRequest request, final HttpServletResponse response, final Instance instance) {

final String identifier = getRuleId(request);

request.setAttribute(RULE_REQUEST_ID, Objects.nonNull(request.getAttribute(RULE_REQUEST_ID)) ?
request.getAttribute(RULE_REQUEST_ID) : UUIDUtil.uuid());
final HashMap<String, String> objectDetail = new HashMap<>();
final Map<String, Serializable> userEventPayload = new HashMap<>();

userEventPayload.put(RULE_ID, Objects.nonNull(instance.objectId) ? instance.objectId : identifier);

objectDetail.put(RULE_ID, identifier);
objectDetail.put(RULE_OBJECT_CONTENT_TYPE_VAR_NAME, Objects.nonNull(instance.objectType) ? instance.objectType : RULE_CONTENT);
userEventPayload.put(RULE_OBJECT, objectDetail);
userEventPayload.put(RULE_EVENT_TYPE1, instance.eventType);
webEventsCollectorService.fireCollectorsAndEmitEvent(request, response, USER_CUSTOM_DEFINED_REQUEST_MATCHER, userEventPayload);

return true;
}

private String getRuleId(final HttpServletRequest request) {

return Optional.of(request.getAttribute(WebKeys.RULES_ENGINE_PARAM_CURRENT_RULE_ID)).orElseGet(()-> StringPool.UNKNOWN).toString();
}

public class Instance implements RuleComponentInstance {

private final String eventType;
private final String objectType;
private final String objectId;

public Instance(final Map<String, ParameterModel> parameters) {

DotPreconditions.checkNotNull(parameters, "parameters can't be null");
this.eventType = parameters.getOrDefault(RULE_EVENT_TYPE, new ParameterModel()).getValue();
this.objectType = parameters.getOrDefault(RULE_OBJECT_TYPE, new ParameterModel()).getValue();
this.objectId = parameters.getOrDefault(RULE_OBJECT_ID, new ParameterModel()).getValue();
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,9 @@ public static void fireRules(final HttpServletRequest request, final HttpServlet
* Triggers a specific category of Rules associated to the specified parent
* object based on the requested resource.
*
* @param req
* @param request
* - The {@link HttpServletRequest} object.
* @param res
* @param response
* - The {@link HttpServletResponse} object.
* @param parent
* - The object whose associated rules will be fired. For
Expand All @@ -159,62 +159,65 @@ public static void fireRules(final HttpServletRequest request, final HttpServlet
* - The category of the rules that will be fired: Every page,
* every request, etc.
*/
public static void fireRules(HttpServletRequest req, HttpServletResponse res, Ruleable parent, Rule.FireOn fireOn) {
public static void fireRules(final HttpServletRequest request, final HttpServletResponse response,
final Ruleable parent, final Rule.FireOn fireOn) {

//Check for the proper license level, the rules engine is an enterprise feature only
if ( LicenseUtil.getLevel() < LicenseLevel.STANDARD.level ) {
return;
}
if(res.isCommitted()) {
if(response.isCommitted()) {
return;
}
if (!UtilMethods.isSet(req)) {
if (!UtilMethods.isSet(request)) {
throw new DotRuntimeException("ERROR: HttpServletRequest is null");
}

// do not run rules in admin mode
PageMode mode= PageMode.get(req);
PageMode mode= PageMode.get(request);
if(mode.isAdmin) {
final boolean fireRulesFromParameter =Try.of(()->Boolean.valueOf
(req.getParameter("fireRules"))).getOrElse(false);
(request.getParameter("fireRules"))).getOrElse(false);
final boolean fireRulesFromAttribute =Try.of(()-> Boolean.valueOf((Boolean)
req.getAttribute("fireRules"))).getOrElse(false);
request.getAttribute("fireRules"))).getOrElse(false);

if(!fireRulesFromParameter && !fireRulesFromAttribute) {
return;
}
}

final Set<String> alreadyFiredRulesFor =req.getAttribute(DOT_RULES_FIRED_ALREADY)!=null?(Set<String>)req.getAttribute(DOT_RULES_FIRED_ALREADY):new HashSet<String>();
final Set<String> alreadyFiredRulesFor =request.getAttribute(DOT_RULES_FIRED_ALREADY)!=null?(Set<String>)request.getAttribute(DOT_RULES_FIRED_ALREADY):new HashSet<String>();
final String ruleRunKey = parent.getIdentifier() +"_"+ fireOn.name();
if(alreadyFiredRulesFor.contains(ruleRunKey)) {
Logger.warn(RulesEngine.class, "we have already run the rules for:" + ruleRunKey);
return;
}
alreadyFiredRulesFor.add(ruleRunKey);
req.setAttribute(DOT_RULES_FIRED_ALREADY,alreadyFiredRulesFor);
request.setAttribute(DOT_RULES_FIRED_ALREADY,alreadyFiredRulesFor);

if (SKIP_RULES_EXECUTION.equalsIgnoreCase(req.getParameter(WebKeys.RULES_ENGINE_PARAM))
|| SKIP_RULES_EXECUTION.equalsIgnoreCase(String.valueOf(req.getParameter(WebKeys.RULES_ENGINE_PARAM)))) {
if (SKIP_RULES_EXECUTION.equalsIgnoreCase(request.getParameter(WebKeys.RULES_ENGINE_PARAM))
|| SKIP_RULES_EXECUTION.equalsIgnoreCase(String.valueOf(request.getParameter(WebKeys.RULES_ENGINE_PARAM)))) {
return;
}
if (!UtilMethods.isSet(parent)) {
return;
}

User systemUser = APILocator.systemUser();
final User systemUser = APILocator.systemUser();

try {

Set<Rule> rules = APILocator.getRulesAPI().getRulesByParentFireOn(parent.getIdentifier(), systemUser, false,
final Set<Rule> rules = APILocator.getRulesAPI().getRulesByParentFireOn(parent.getIdentifier(), systemUser, false,
fireOn);
for (Rule rule : rules) {
for (final Rule rule : rules) {
try {

request.setAttribute(WebKeys.RULES_ENGINE_PARAM_CURRENT_RULE_ID, rule.getId());
long before = System.currentTimeMillis();
rule.checkValid(); // @todo ggranum: this should actually be done on writing to the DB, or at worst reading from.
boolean evaled = rule.evaluate(req, res);
boolean evaled = rule.evaluate(request, response);

if(res.isCommitted()) {
if(response.isCommitted()) {
return;
}
if (evaled) {
Expand All @@ -228,7 +231,7 @@ public static void fireRules(HttpServletRequest req, HttpServletResponse res, Ru
rCopy.setShortCircuit(rule.isShortCircuit());
rCopy.setModDate(rule.getModDate());

trackFiredRule(rCopy, req);
trackFiredRule(rCopy, request);
}
long after = System.currentTimeMillis();
if((after - before) > SLOW_RULE_LOG_MIN) {
Expand Down
2 changes: 2 additions & 0 deletions dotCMS/src/main/java/com/dotmarketing/util/WebKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ public static enum WorkflowStatuses { OPEN, RESOLVED, CANCELLED };
public static final String RULES_ACTIONLET_CLASSES = "RULES_ACTIONLET_CLASSES";
public static final String RULES_CONDITIONLET_VISITEDURLS = "RULES_CONDITIONLET_VISITEDURLS";
public static final String RULES_ENGINE_PARAM = "dotRules";
// stores the rule id that is currently being evaluated
public static final String RULES_ENGINE_PARAM_CURRENT_RULE_ID = "dotCurrentRuleId";
public static final String RULES_ENGINE_FIRE_LIST = "dotRulesFired";

//ADMIN CONTROL
Expand Down
3 changes: 3 additions & 0 deletions dotCMS/src/main/java/com/liferay/util/StringPool.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,7 @@ public class StringPool {

public static final String FALSE = Boolean.FALSE.toString();

public static final String UNKNOWN = "UNKNOWN";


}
2 changes: 2 additions & 0 deletions dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ api.sites.ruleengine.rules.inputs.name.placeholder=Describe the Rule
api.sites.ruleengine.rules.inputs.onOff.off.label=Off
api.sites.ruleengine.rules.inputs.onOff.on.label=On
api.sites.ruleengine.rules.inputs.onOff.tip=Enable this rule
api.system.ruleengine.actionlet.analytics_user_event.name=Fire Analytics Event

api.system.ruleengine.actionlet.send_redirect.input.url=Redirect URL
api.system.ruleengine.actionlet.StopProcessingActionlet.name=Stop Processing
api.system.ruleengine.actionlet.StopProcessingActionlet.inputs.return-code.placeholder=200 OK
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.dotmarketing.portlets.rules.actionlet;

import com.dotcms.UnitTestBase;
import com.dotcms.analytics.track.collectors.Collector;
import com.dotcms.analytics.track.collectors.WebEventsCollectorService;
import com.dotcms.analytics.track.matchers.RequestMatcher;
import com.dotmarketing.portlets.rules.model.ParameterModel;
import com.dotmarketing.util.WebKeys;
import org.junit.Assert;
import org.junit.Test;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* This actionlet allows to fire an user event rule to the analytics backend.
* @author jsanca
*/
public class RuleAnalyticsFireUserEventActionletTest extends UnitTestBase {

/**
* * Method to test: RuleAnalyticsFireUserEventActionlet.evaluate
* * Given Scenario: Creates the context and send the information to the rule
* * ExpectedResult: The Payload created has to have the expected values based on the inputs
* @throws Exception
*/
@Test
public void testActionletSetsFireUserEventOnHappyPath() throws Exception {

final WebEventsCollectorService webEventsCollectorService = new WebEventsCollectorService() {
@Override
public void fireCollectors(HttpServletRequest request, HttpServletResponse response, RequestMatcher requestMatcher) {

}

@Override
public void addCollector(Collector... collectors) {

}

@Override
public void removeCollector(String collectorId) {

}

@Override
public void fireCollectorsAndEmitEvent(HttpServletRequest request, HttpServletResponse response,
RequestMatcher requestMatcher, Map<String, Serializable> userEventPayload) {

Assert.assertNotNull(userEventPayload);
Assert.assertEquals("page", userEventPayload.get("event_type"));
Assert.assertEquals("345", userEventPayload.get("id"));

final Map<String, String> object = (Map<String, String>) userEventPayload.get("object");
Assert.assertNotNull(object);
Assert.assertEquals("123", object.get("id"));
Assert.assertEquals("CONTENT", object.get("object_content_type_var_name"));
}
};

final RuleAnalyticsFireUserEventActionlet analyticsFireUserEventActionlet = new RuleAnalyticsFireUserEventActionlet(webEventsCollectorService);
final Map<String, ParameterModel> params = new HashMap<>();
params.put(RuleAnalyticsFireUserEventActionlet.RULE_EVENT_TYPE, new ParameterModel(RuleAnalyticsFireUserEventActionlet.RULE_EVENT_TYPE, "page"));
params.put(RuleAnalyticsFireUserEventActionlet.RULE_OBJECT_TYPE, new ParameterModel());
params.put(RuleAnalyticsFireUserEventActionlet.RULE_OBJECT_ID, new ParameterModel(RuleAnalyticsFireUserEventActionlet.RULE_EVENT_TYPE, "345"));

final HttpServletRequest request = mock(HttpServletRequest.class);
final HttpServletResponse response = mock(HttpServletResponse.class);
when(request.getAttribute(WebKeys.RULES_ENGINE_PARAM_CURRENT_RULE_ID)).thenReturn("123");
final RuleAnalyticsFireUserEventActionlet.Instance instance = analyticsFireUserEventActionlet.instanceFrom(params);

analyticsFireUserEventActionlet.evaluate(request, response, instance);
}

}

0 comments on commit b5017f1

Please sign in to comment.