[CST-18963] Refactors matomo event handler to track bitstream view

This commit is contained in:
Vincenzo Mecca
2025-02-21 13:27:36 +01:00
parent 611f353b54
commit 117457ce16
6 changed files with 169 additions and 23 deletions

View File

@@ -12,8 +12,10 @@ import java.io.InputStream;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jakarta.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
@@ -496,4 +498,14 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl<Bitstream> imp
public Long getLastModified(Bitstream bitstream) throws IOException {
return bitstreamStorageService.getLastModified(bitstream);
}
@Override
public boolean isInBundle(Bitstream bitstream, java.util.Collection<String> bundleNames) throws SQLException {
Set<String> bundles =
bitstream.getBundles()
.stream()
.map(Bundle::getName)
.collect(Collectors.toSet());
return bundleNames.stream().anyMatch(bundles::contains);
}
}

View File

@@ -235,4 +235,14 @@ public interface BitstreamService extends DSpaceObjectService<Bitstream>, DSpace
*/
@Nullable
Long getLastModified(Bitstream bitstream) throws IOException;
/**
* Checks if the given bitstream is inside one of the bundle
*
* @param bitstream bitstream to verify
* @param bundleNames names of the bundles to serch for
* @return true if is in one of the bundles, false otherwise
* @throws SQLException
*/
boolean isInBundle(Bitstream bitstream, java.util.Collection<String> bundleNames) throws SQLException;
}

View File

@@ -11,6 +11,7 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -23,8 +24,8 @@ import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.google.client.GoogleAnalyticsClient;
@@ -57,6 +58,9 @@ public class GoogleAsyncEventListener extends AbstractUsageEventListener {
@Autowired
private ClientInfoService clientInfoService;
@Autowired
private BitstreamService bitstreamService;
@Autowired
private List<GoogleAnalyticsClient> googleAnalyticsClients;
@@ -181,25 +185,35 @@ public class GoogleAsyncEventListener extends AbstractUsageEventListener {
*/
private boolean isContentBitstream(UsageEvent usageEvent) {
// check if event is a VIEW event and object is a Bitstream
if (usageEvent.getAction() == UsageEvent.Action.VIEW
&& usageEvent.getObject().getType() == Constants.BITSTREAM) {
if (!isBitstreamView(usageEvent)) {
return false;
}
// check if bitstream belongs to a configured bundle
List<String> allowedBundles = List.of(configurationService
.getArrayProperty("google-analytics.bundles", new String[]{Constants.CONTENT_BUNDLE_NAME}));
Set<String> allowedBundles =
Set.of(
configurationService.getArrayProperty(
"google-analytics.bundles",
new String[]{Constants.CONTENT_BUNDLE_NAME}
)
);
if (allowedBundles.contains("none")) {
// GA events for bitstream views were turned off in config
return false;
}
List<String> bitstreamBundles;
return isInBundle((Bitstream) usageEvent.getObject(), allowedBundles);
}
private boolean isInBundle(Bitstream bitstream, Set<String> allowedBundles) {
try {
bitstreamBundles = ((Bitstream) usageEvent.getObject())
.getBundles().stream().map(Bundle::getName).collect(Collectors.toList());
return this.bitstreamService.isInBundle(bitstream, allowedBundles);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
return allowedBundles.stream().anyMatch(bitstreamBundles::contains);
}
return false;
private boolean isBitstreamView(UsageEvent usageEvent) {
return usageEvent.getAction() == UsageEvent.Action.VIEW
&& usageEvent.getObject().getType() == Constants.BITSTREAM;
}
private boolean isGoogleAnalyticsKeyNotConfigured() {

View File

@@ -7,10 +7,15 @@
*/
package org.dspace.matomo;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Bitstream;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants;
import org.dspace.services.ConfigurationService;
import org.dspace.services.model.Event;
import org.dspace.usage.AbstractUsageEventListener;
@@ -28,14 +33,17 @@ public class MatomoEventListener extends AbstractUsageEventListener {
private static final Logger log = LogManager.getLogger(MatomoEventListener.class);
private final ConfigurationService configurationService;
private final BitstreamService bitstreamService;
private final List<MatomoUsageEventHandler> matomoUsageEventHandlers;
public MatomoEventListener(
@Autowired List<MatomoUsageEventHandler> matomoUsageEventHandlers,
@Autowired ConfigurationService configurationService
@Autowired ConfigurationService configurationService,
@Autowired BitstreamService bitstreamService
) {
this.matomoUsageEventHandlers = matomoUsageEventHandlers;
this.configurationService = configurationService;
this.bitstreamService = bitstreamService;
}
@Override
@@ -49,6 +57,10 @@ public class MatomoEventListener extends AbstractUsageEventListener {
return;
}
if (!isContentBitstream(usageEvent)) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Usage event received {}", event.getName());
}
@@ -64,4 +76,48 @@ public class MatomoEventListener extends AbstractUsageEventListener {
return this.configurationService.getBooleanProperty("matomo.enabled", false);
}
/**
* Verifies if the usage event is a content bitstream view event, by checking if:
* <ul>
* <li>the usage event is a view event</li>
* <li>the object of the usage event is a bitstream</li>
* <li>the bitstream belongs to one of the configured bundles (fallback: ORIGINAL bundle)</li>
* </ul>
*/
private boolean isContentBitstream(UsageEvent usageEvent) {
// check if event is a VIEW event and object is a Bitstream
if (!isBitstreamView(usageEvent)) {
return false;
}
// check if bitstream belongs to a configured bundle
Set<String> allowedBundles = getTrackedBundles();
if (allowedBundles.contains("none")) {
// events for bitstream views were turned off in config
return false;
}
return isInBundle(((Bitstream) usageEvent.getObject()), allowedBundles);
}
private Set<String> getTrackedBundles() {
return Set.of(
configurationService.getArrayProperty(
"matomo.track.bundles",
new String[] {Constants.CONTENT_BUNDLE_NAME}
)
);
}
protected boolean isInBundle(Bitstream bitstream, Set<String> allowedBundles) {
try {
return this.bitstreamService.isInBundle(bitstream, allowedBundles);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
private boolean isBitstreamView(UsageEvent usageEvent) {
return usageEvent.getAction() == UsageEvent.Action.VIEW
&& usageEvent.getObject().getType() == Constants.BITSTREAM;
}
}

View File

@@ -7,9 +7,15 @@
*/
package org.dspace.matomo;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import org.dspace.AbstractUnitTest;
import org.dspace.content.Bitstream;
import org.dspace.content.Item;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants;
import org.dspace.services.ConfigurationService;
import org.dspace.usage.UsageEvent;
import org.junit.Before;
@@ -25,13 +31,15 @@ public class MatomoEventListenerTest extends AbstractUnitTest {
MatomoSyncEventHandler matomoHandler2;
@Mock
ConfigurationService configurationService;
@Mock
BitstreamService bitstreamService;
MatomoEventListener matomoEventListener;
@Before
public void setUp() throws Exception {
matomoEventListener =
new MatomoEventListener(List.of(matomoHandler1, matomoHandler2), configurationService);
new MatomoEventListener(List.of(matomoHandler1, matomoHandler2), configurationService, bitstreamService);
}
@Test
@@ -46,17 +54,60 @@ public class MatomoEventListenerTest extends AbstractUnitTest {
@Test
public void testHandleEvent() {
public void testDontHandleGenericViewEventWithMatomoEnabled() {
UsageEvent event = Mockito.mock(UsageEvent.class);
Mockito.when(event.getAction()).thenReturn(UsageEvent.Action.VIEW);
Mockito.when(event.getObject()).thenReturn(Mockito.spy(Item.class));
Mockito.when(configurationService.getBooleanProperty("matomo.enabled", false))
.thenReturn(true);
matomoEventListener.receiveEvent(event);
Mockito.verifyNoInteractions(matomoHandler1);
Mockito.verifyNoInteractions(matomoHandler2);
}
@Test
public void testHandleBitstreamViewEvent() throws SQLException {
UsageEvent event = Mockito.mock(UsageEvent.class);
Mockito.when(event.getAction()).thenReturn(UsageEvent.Action.VIEW);
Bitstream bitstream = Mockito.spy(Bitstream.class);
Mockito.when(bitstreamService.isInBundle(Mockito.eq(bitstream), Mockito.eq(Set.of(Constants.CONTENT_BUNDLE_NAME))))
.thenReturn(true);
Mockito.when(event.getObject()).thenReturn(bitstream);
Mockito.when(configurationService.getBooleanProperty(Mockito.eq("matomo.enabled"), Mockito.eq(false)))
.thenReturn(true);
Mockito.when(configurationService.getArrayProperty(Mockito.eq("matomo.track.bundles"), Mockito.any()))
.thenReturn(new String[] { });
matomoEventListener.receiveEvent(event);
Mockito.verifyNoInteractions(matomoHandler1);
Mockito.verifyNoInteractions(matomoHandler2);
// none bundle, will skip processing
Mockito.when(configurationService.getArrayProperty(Mockito.eq("matomo.track.bundles"), Mockito.any()))
.thenReturn(new String[] {"none"});
matomoEventListener.receiveEvent(event);
Mockito.verifyNoMoreInteractions(matomoHandler1);
Mockito.verifyNoMoreInteractions(matomoHandler2);
// default ( original bundle only ) then proceed with the invocation
Mockito.when(configurationService.getArrayProperty(Mockito.eq("matomo.track.bundles"), Mockito.any()))
.thenReturn(new String[] { Constants.CONTENT_BUNDLE_NAME });
matomoEventListener.receiveEvent(event);
Mockito.verify(matomoHandler1, Mockito.times(1)).handleEvent(event);
Mockito.verify(matomoHandler2, Mockito.times(1)).handleEvent(event);
Mockito.verifyNoMoreInteractions(matomoHandler1, matomoHandler2);
}
}

View File

@@ -6,6 +6,9 @@
matomo.enabled = false
# Configured `siteid` inside the matomo dashboard
matomo.request.siteid = 1
# Specifies bitstream's bundle that will be tracked ( default is ORIGINAL )
# Add 'none' to disable the tracking for bitstreams
# matomo.track.bundles = ORIGINAL
#---------------------------------------------------------------#
#----------------MATOMO CLIENTS CONFIGURATION-------------------#