Skip to content

Commit

Permalink
[CST-18963] Refactors matomo event handler to track bitstream view
Browse files Browse the repository at this point in the history
  • Loading branch information
vins01-4science committed Feb 21, 2025
1 parent 611f353 commit 117457c
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
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;
Expand Down Expand Up @@ -496,4 +498,14 @@ public List<Bitstream> getNotReferencedBitstreams(Context context) throws SQLExc
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,14 @@ public InputStream retrieve(Context context, Bitstream bitstream)
*/
@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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;

Expand All @@ -23,8 +24,8 @@
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;
Expand Down Expand Up @@ -57,6 +58,9 @@ public class GoogleAsyncEventListener extends AbstractUsageEventListener {
@Autowired
private ClientInfoService clientInfoService;

@Autowired
private BitstreamService bitstreamService;

@Autowired
private List<GoogleAnalyticsClient> googleAnalyticsClients;

Expand Down Expand Up @@ -181,25 +185,35 @@ private String getDocumentPath(HttpServletRequest request) {
*/
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) {
// check if bitstream belongs to a configured bundle
List<String> allowedBundles = List.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;
try {
bitstreamBundles = ((Bitstream) usageEvent.getObject())
.getBundles().stream().map(Bundle::getName).collect(Collectors.toList());
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
return allowedBundles.stream().anyMatch(bitstreamBundles::contains);
if (!isBitstreamView(usageEvent)) {
return false;
}
// check if bitstream belongs to a configured bundle
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;
}
return isInBundle((Bitstream) usageEvent.getObject(), allowedBundles);
}

private boolean isInBundle(Bitstream bitstream, Set<String> allowedBundles) {
try {
return this.bitstreamService.isInBundle(bitstream, allowedBundles);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
return false;
}

private boolean isBitstreamView(UsageEvent usageEvent) {
return usageEvent.getAction() == UsageEvent.Action.VIEW
&& usageEvent.getObject().getType() == Constants.BITSTREAM;
}

private boolean isGoogleAnalyticsKeyNotConfigured() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -49,6 +57,10 @@ public void receiveEvent(Event event) {
return;
}

if (!isContentBitstream(usageEvent)) {
return;
}

if (log.isDebugEnabled()) {
log.debug("Usage event received {}", event.getName());
}
Expand All @@ -64,4 +76,48 @@ private boolean matomoEnabled() {
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -46,17 +54,60 @@ public void testDisabledMatomo() {


@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);

}

}
3 changes: 3 additions & 0 deletions dspace/config/modules/matomo.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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-------------------#
Expand Down

0 comments on commit 117457c

Please sign in to comment.