Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 30076 analytics ff #30124

Merged
merged 47 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
cf827da
Revert "fix: #28563 removing template ajax (#28572)"
jdotcms May 15, 2024
fb39637
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 11, 2024
af6fed1
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 14, 2024
96dd743
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 18, 2024
fb71cf9
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 24, 2024
ae1d5e1
Merge branches 'master' and 'master' of github.com:dotCMS/core
jdotcms Jun 25, 2024
b523af6
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 27, 2024
95de159
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 27, 2024
679b600
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jun 28, 2024
4bb207b
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 2, 2024
320b4b1
Merge branches 'master' and 'master' of github.com:dotCMS/core
jdotcms Jul 2, 2024
ed6a751
Merge branches 'master' and 'master' of github.com:dotCMS/core
jdotcms Jul 3, 2024
02a6e96
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 4, 2024
5479786
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 5, 2024
0762192
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 9, 2024
2a61c7f
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 11, 2024
0afad59
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 17, 2024
9c510e7
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 18, 2024
24a5827
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 19, 2024
7468cd7
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 22, 2024
0388022
Merge branch 'master' of github.com:dotCMS/core
jdotcms Jul 29, 2024
162ade1
Merge branch 'master' of github.com:dotCMS/core
jdotcms Aug 19, 2024
1e9be41
Merge branch 'master' of github.com:dotCMS/core
jdotcms Aug 21, 2024
17cac16
Merge branch 'master' of github.com:dotCMS/core
jdotcms Aug 21, 2024
ff615f0
Merge branch 'master' of github.com:dotCMS/core
jdotcms Aug 27, 2024
431befb
Merge branch 'master' of github.com:dotCMS/core
jdotcms Aug 29, 2024
548d8cd
Merge branch 'master' of github.com:dotCMS/core
jdotcms Aug 30, 2024
f0647f6
Merge branch 'master' of github.com:dotCMS/core
jdotcms Sep 6, 2024
3fbc5a3
Merge branch 'master' of github.com:dotCMS/core
jdotcms Sep 18, 2024
9538154
Merge branch 'master' of github.com:dotCMS/core
jdotcms Sep 24, 2024
dd7c61f
#30076 adding the flag to run or not the analytics interceptor
jdotcms Sep 24, 2024
c0ddb1f
#30076 adding the flag to run or not the analytics interceptor
jdotcms Sep 24, 2024
a35875b
#30076 adding the flag to run or not the analytics interceptor
jdotcms Sep 24, 2024
b48c5aa
#30076 adding the flag to run or not the analytics interceptor
jdotcms Sep 24, 2024
08b2a0a
#30076 adding the flag to run or not the analytics interceptor
jdotcms Sep 24, 2024
ae3810b
#30076 adding test of Analytics interceptor
jdotcms Sep 24, 2024
b83d959
#30076 adding test of Analytics interceptor
jdotcms Sep 24, 2024
58547ae
#30076 adding test of Analytics interceptor
jdotcms Sep 24, 2024
2f2aa2e
Merge branch 'master' into issue-30076-analytics-ff
dsilvam Sep 25, 2024
e3e48fb
#30076 adding fixes
jdotcms Sep 25, 2024
63b2285
Merge branch 'issue-30076-analytics-ff' of github.com:dotCMS/core int…
jdotcms Sep 25, 2024
c549f94
Merge branch 'master' into issue-30076-analytics-ff
jdotcms Sep 26, 2024
ed7c18b
Merge branch 'master' into issue-30076-analytics-ff
dsilvam Sep 26, 2024
f8a86fe
#30076 adding a fix, it seems we can not ask for a system user when c…
jdotcms Sep 26, 2024
e844ad4
Merge branch 'issue-30076-analytics-ff' of github.com:dotCMS/core int…
jdotcms Sep 26, 2024
79723ed
#30076 sonarq
jdotcms Sep 26, 2024
2657501
#30076 fixing an init issue on test
jdotcms Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,50 +1,80 @@
package com.dotcms.analytics.track;

import com.dotcms.analytics.app.AnalyticsApp;
import com.dotcms.analytics.track.collectors.WebEventsCollectorServiceFactory;
import com.dotcms.analytics.track.matchers.FilesRequestMatcher;
import com.dotcms.analytics.track.matchers.PagesAndUrlMapsRequestMatcher;
import com.dotcms.analytics.track.matchers.RequestMatcher;
import com.dotcms.analytics.track.matchers.VanitiesRequestMatcher;
import com.dotcms.business.SystemTableUpdatedKeyEvent;
import com.dotcms.filters.interceptor.Result;
import com.dotcms.filters.interceptor.WebInterceptor;
import com.dotcms.security.apps.AppsAPI;
import com.dotcms.system.event.local.model.EventSubscriber;
import com.dotcms.util.CollectionsUtils;
import com.dotcms.util.WhiteBlackList;
import com.dotmarketing.beans.Host;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.web.HostWebAPI;
import com.dotmarketing.business.web.WebAPILocator;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UUIDUtil;
import com.liferay.util.StringPool;
import io.vavr.control.Try;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;

/**
* Web Interceptor to track analytics
* @author jsanca
*/
public class AnalyticsTrackWebInterceptor implements WebInterceptor {
public class AnalyticsTrackWebInterceptor implements WebInterceptor, EventSubscriber<SystemTableUpdatedKeyEvent> {

private final static String ANALYTICS_TURNED_ON_KEY = "ANALYTICS_TURNED_ON";
private final static Map<String, RequestMatcher> requestMatchersMap = new ConcurrentHashMap<>();
private final HostWebAPI hostWebAPI;
private final AppsAPI appsAPI;

/// private static final String[] DEFAULT_BLACKLISTED_PROPS = new String[]{"^/api/*"};
jdotcms marked this conversation as resolved.
Show resolved Hide resolved
private static final String[] DEFAULT_BLACKLISTED_PROPS = new String[]{StringPool.BLANK};
private final WhiteBlackList whiteBlackList = new WhiteBlackList.Builder()
.addWhitePatterns(Config.getStringArrayProperty("ANALYTICS_WHITELISTED_KEYS",
new String[]{StringPool.BLANK})) // allows everything
.addBlackPatterns(CollectionsUtils.concat(Config.getStringArrayProperty( // except this
"ANALYTICS_BLACKLISTED_KEYS", new String[]{}), DEFAULT_BLACKLISTED_PROPS)).build();
private final WhiteBlackList whiteBlackList;
private final AtomicBoolean isTurnedOn;

public AnalyticsTrackWebInterceptor() {

addRequestMatcher(
this(WebAPILocator.getHostWebAPI(), APILocator.getAppsAPI(),
new WhiteBlackList.Builder()
.addWhitePatterns(Config.getStringArrayProperty("ANALYTICS_WHITELISTED_KEYS",
new String[]{StringPool.BLANK})) // allows everything
.addBlackPatterns(CollectionsUtils.concat(Config.getStringArrayProperty( // except this
"ANALYTICS_BLACKLISTED_KEYS", new String[]{}), DEFAULT_BLACKLISTED_PROPS)).build(),
new AtomicBoolean(Config.getBooleanProperty(ANALYTICS_TURNED_ON_KEY, true)),
jdotcms marked this conversation as resolved.
Show resolved Hide resolved
new PagesAndUrlMapsRequestMatcher(),
new FilesRequestMatcher(),
// new RulesRedirectsRequestMatcher(),
// new RulesRedirectsRequestMatcher(),
new VanitiesRequestMatcher());

}

public AnalyticsTrackWebInterceptor(final HostWebAPI hostWebAPI,
final AppsAPI appsAPI,
final WhiteBlackList whiteBlackList,
final AtomicBoolean isTurnedOn,
final RequestMatcher... requestMatchers) {

this.hostWebAPI = hostWebAPI;
this.appsAPI = appsAPI;
this.whiteBlackList = whiteBlackList;
this.isTurnedOn = isTurnedOn;
addRequestMatcher(requestMatchers);
}

/**
Expand All @@ -70,19 +100,59 @@ public static void removeRequestMatcher(final String requestMatcherId) {
@Override
public Result intercept(final HttpServletRequest request, final HttpServletResponse response) throws IOException {

if (whiteBlackList.isAllowed(request.getRequestURI())) {
final Optional<RequestMatcher> matcherOpt = this.anyMatcher(request, response, RequestMatcher::runBeforeRequest);
if (matcherOpt.isPresent()) {
try {
if (isAllowed(request)) {
final Optional<RequestMatcher> matcherOpt = this.anyMatcher(request, response, RequestMatcher::runBeforeRequest);
if (matcherOpt.isPresent()) {

addRequestId (request);
Logger.debug(this, () -> "intercept, Matched: " + matcherOpt.get().getId() + " request: " + request.getRequestURI());
fireNext(request, response, matcherOpt.get());
addRequestId(request);
Logger.debug(this, () -> "intercept, Matched: " + matcherOpt.get().getId() + " request: " + request.getRequestURI());
fireNext(request, response, matcherOpt.get());
}
}
} catch (Exception e) {
Logger.error(this, e.getMessage(), e);
}

return Result.NEXT;
}

/**
* If the feature flag under {@link #ANALYTICS_TURNED_ON_KEY} is on
* and there is any configuration for the analytics app
* and the white black list allowed the current request
* @param request
* @return
*/
private boolean isAllowed(final HttpServletRequest request) {

return isTurnedOn.get() &&
anyConfig(request) &&
whiteBlackList.isAllowed(request.getRequestURI());
}

private boolean anyConfig(final HttpServletRequest request) {

final Host currentSite = this.hostWebAPI.getCurrentHostNoThrow(request);

return anySecrets(currentSite);

}

/**
* Returns true if the host or the system host has any secrets for the analytics app.
* @param host
* @return
*/
private boolean anySecrets (final Host host) {

return Try.of(
() ->
this.appsAPI.getSecrets(
AnalyticsApp.ANALYTICS_APP_KEY, true, host, APILocator.systemUser()).isPresent())
.getOrElseGet(e -> false);
}

private void addRequestId(final HttpServletRequest request) {
if (null == request.getAttribute("requestId")) {
request.setAttribute("requestId", UUIDUtil.uuid());
Expand All @@ -92,14 +162,18 @@ private void addRequestId(final HttpServletRequest request) {
@Override
public boolean afterIntercept(final HttpServletRequest request, final HttpServletResponse response) {

if (whiteBlackList.isAllowed(request.getRequestURI())) {
final Optional<RequestMatcher> matcherOpt = this.anyMatcher(request, response, RequestMatcher::runAfterRequest);
if (matcherOpt.isPresent()) {
try {
if (isAllowed(request)) {
final Optional<RequestMatcher> matcherOpt = this.anyMatcher(request, response, RequestMatcher::runAfterRequest);
if (matcherOpt.isPresent()) {

addRequestId (request);
Logger.debug(this, () -> "afterIntercept, Matched: " + matcherOpt.get().getId() + " request: " + request.getRequestURI());
fireNext(request, response, matcherOpt.get());
addRequestId(request);
Logger.debug(this, () -> "afterIntercept, Matched: " + matcherOpt.get().getId() + " request: " + request.getRequestURI());
fireNext(request, response, matcherOpt.get());
}
}
} catch (Exception e) {
Logger.error(this, e.getMessage(), e);
}

return true;
Expand Down Expand Up @@ -128,4 +202,10 @@ protected void fireNext(final HttpServletRequest request, final HttpServletRespo
}


@Override
public void notify(final SystemTableUpdatedKeyEvent event) {
if (event.getKey().contains(ANALYTICS_TURNED_ON_KEY)) {
isTurnedOn.set(Config.getBooleanProperty(ANALYTICS_TURNED_ON_KEY, true));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dotmarketing.filters;

import com.dotcms.analytics.track.AnalyticsTrackWebInterceptor;
import com.dotcms.business.SystemTableUpdatedKeyEvent;
import com.dotcms.ema.EMAWebInterceptor;
import com.dotcms.filters.interceptor.AbstractWebInterceptorSupportFilter;
import com.dotcms.filters.interceptor.WebInterceptorDelegate;
Expand All @@ -9,7 +10,9 @@
import com.dotcms.jitsu.EventLogWebInterceptor;
import com.dotcms.prerender.PreRenderSEOWebInterceptor;
import com.dotcms.security.multipart.MultiPartRequestSecurityWebInterceptor;
import com.dotcms.system.event.local.business.LocalSystemEventsAPI;
import com.dotcms.variant.business.web.CurrentVariantWebInterceptor;
import com.dotmarketing.business.APILocator;

import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
Expand All @@ -33,14 +36,17 @@ private void addInterceptors(final FilterConfig config) {
final WebInterceptorDelegate delegate =
this.getDelegate(config.getServletContext());

final AnalyticsTrackWebInterceptor analyticsTrackWebInterceptor = new AnalyticsTrackWebInterceptor();
delegate.add(new MultiPartRequestSecurityWebInterceptor());
delegate.add(new PreRenderSEOWebInterceptor());
delegate.add(new EMAWebInterceptor());
delegate.add(new GraphqlCacheWebInterceptor());
delegate.add(new ResponseMetaDataWebInterceptor());
delegate.add(new EventLogWebInterceptor());
delegate.add(new CurrentVariantWebInterceptor());
//delegate.add(new AnalyticsTrackWebInterceptor()); // turn on when needed.
jdotcms marked this conversation as resolved.
Show resolved Hide resolved
delegate.add(analyticsTrackWebInterceptor);

APILocator.getLocalSystemEventsAPI().subscribe(SystemTableUpdatedKeyEvent.class, analyticsTrackWebInterceptor);
} // addInterceptors.

} // E:O:F:InterceptorFilter.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.dotcms.analytics.track;

import com.dotcms.analytics.app.AnalyticsApp;
import com.dotcms.analytics.track.matchers.RequestMatcher;
import com.dotcms.security.apps.AppSecrets;
import com.dotcms.security.apps.AppsAPI;
import com.dotcms.util.WhiteBlackList;
import com.dotmarketing.beans.Host;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.web.HostWebAPI;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
import com.liferay.util.StringPool;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* This test is for the AnalyticsTrackWebInterceptor class.
* @author jsanca
*/
public class AnalyticsTrackWebInterceptorTest {

private final class TestMatcher implements RequestMatcher {

final MutableBoolean wasMatcherCalled = new MutableBoolean(false);

@Override
public boolean match(HttpServletRequest request, HttpServletResponse response) {
wasMatcherCalled.setValue(true);
return true;
}

public boolean wasCalled() {
return wasMatcherCalled.booleanValue();
}

@Override
public boolean runBeforeRequest() {
return true;
}

}

/**
* Method to test: AnalyticsTrackWebInterceptor#intercept
* Given Scenario: the feature flag is off, so the matcher test should be not called
* ExpectedResult: The test matcher should be not called
*/
@Test
public void test_intercept_feature_flag_turn_off() throws IOException {

final HostWebAPI hostWebAPI = Mockito.mock(HostWebAPI.class);
final AppsAPI appsAPI = Mockito.mock(AppsAPI.class);
final WhiteBlackList whiteBlackList = Mockito.mock(WhiteBlackList.class);
final AtomicBoolean isTurnedOn = new AtomicBoolean(false); // turn off the feature flag
final TestMatcher testMatcher = new TestMatcher();
final AnalyticsTrackWebInterceptor interceptor = new AnalyticsTrackWebInterceptor(
hostWebAPI, appsAPI, whiteBlackList, isTurnedOn, testMatcher);
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
interceptor.intercept(request, response);

Assert.assertFalse("The test matcher should be not called, ff is off", testMatcher.wasCalled());
}

/**
* Method to test: AnalyticsTrackWebInterceptor#intercept
* Given Scenario: the feature flag is on but not config, so the matcher test should be not called
* ExpectedResult: The test matcher should be not called
*/
@Test
public void test_intercept_feature_flag_turn_on_and_no_analytics_app() throws IOException, DotDataException, DotSecurityException {

final HostWebAPI hostWebAPI = Mockito.mock(HostWebAPI.class);
final AppsAPI appsAPI = Mockito.mock(AppsAPI.class);
final WhiteBlackList whiteBlackList = Mockito.mock(WhiteBlackList.class);
final AtomicBoolean isTurnedOn = new AtomicBoolean(true); // turn on the feature flag
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
final TestMatcher testMatcher = new TestMatcher();
final Host currentHost = new Host();

Mockito.when(hostWebAPI.getCurrentHostNoThrow(request)).thenReturn(currentHost);
Mockito.when(appsAPI.getSecrets(AnalyticsApp.ANALYTICS_APP_KEY,
true, currentHost, APILocator.systemUser())).thenReturn(Optional.empty()); // no config

final AnalyticsTrackWebInterceptor interceptor = new AnalyticsTrackWebInterceptor(
hostWebAPI, appsAPI, whiteBlackList, isTurnedOn, testMatcher);

interceptor.intercept(request, response);

Assert.assertFalse("The test matcher should be not called, no config set", testMatcher.wasCalled());
}

/**
* Method to test: AnalyticsTrackWebInterceptor#intercept
* Given Scenario: the feature flag is on and there is config, so the matcher test should be called
* ExpectedResult: The test matcher should be called
jdotcms marked this conversation as resolved.
Show resolved Hide resolved
*/
@Test
public void test_intercept_feature_flag_turn_on_and_with_analytics_app() throws IOException, DotDataException, DotSecurityException {

final HostWebAPI hostWebAPI = Mockito.mock(HostWebAPI.class);
final AppsAPI appsAPI = Mockito.mock(AppsAPI.class);
final WhiteBlackList whiteBlackList = new WhiteBlackList.Builder()
.addWhitePatterns(new String[]{StringPool.BLANK}) // allows everything
.addBlackPatterns(new String[]{StringPool.BLANK}).build();
final AtomicBoolean isTurnedOn = new AtomicBoolean(true); // turn on the feature flag
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
final TestMatcher testMatcher = new TestMatcher();
final Host currentHost = new Host();
final AppSecrets appSecrets = new AppSecrets.Builder().withKey(AnalyticsApp.ANALYTICS_APP_KEY).build();

Mockito.when(hostWebAPI.getCurrentHostNoThrow(request)).thenReturn(currentHost);
Mockito.when(appsAPI.getSecrets(AnalyticsApp.ANALYTICS_APP_KEY,
true, currentHost, APILocator.systemUser())).thenReturn(Optional.of(appSecrets)); // no config
Mockito.when(request.getRequestURI()).thenReturn("/some-uri");

final AnalyticsTrackWebInterceptor interceptor = new AnalyticsTrackWebInterceptor(
hostWebAPI, appsAPI, whiteBlackList, isTurnedOn, testMatcher);

interceptor.intercept(request, response);

Assert.assertTrue("The test matcher should be called", testMatcher.wasCalled());
}

}
Loading