Skip to content

Commit

Permalink
Merge pull request DSpace#8944 from atmire/w2p-103837_GA4-exclude-non…
Browse files Browse the repository at this point in the history
…-content-bitstream-views-main

Exclude non-content bitstream view events from Google Analytics 4
  • Loading branch information
tdonohue authored Sep 6, 2023
2 parents 4f5b189 + c10e038 commit 2e1f74a
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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.core.Constants;
import org.dspace.core.Context;
Expand Down Expand Up @@ -77,7 +79,7 @@ public void receiveEvent(Event event) {
UsageEvent usageEvent = (UsageEvent) event;
LOGGER.debug("Usage event received " + event.getName());

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

Expand Down Expand Up @@ -171,9 +173,33 @@ private String getDocumentPath(HttpServletRequest request) {
return documentPath;
}

private boolean isNotBitstreamViewEvent(UsageEvent usageEvent) {
return usageEvent.getAction() != UsageEvent.Action.VIEW
|| usageEvent.getObject().getType() != Constants.BITSTREAM;
/**
* 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 (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);
}
return false;
}

private boolean isGoogleAnalyticsKeyNotConfigured() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@
import java.util.List;

import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.builder.BitstreamBuilder;
import org.dspace.builder.BundleBuilder;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.core.Constants;
import org.dspace.google.client.GoogleAnalyticsClient;
import org.dspace.services.ConfigurationService;
import org.junit.After;
Expand All @@ -61,6 +65,8 @@ public class GoogleAsyncEventListenerIT extends AbstractControllerIntegrationTes

private Bitstream bitstream;

private Item item;

private List<GoogleAnalyticsClient> originalGoogleAnalyticsClients;

private GoogleAnalyticsClient firstGaClientMock = mock(GoogleAnalyticsClient.class);
Expand All @@ -80,7 +86,7 @@ public void setup() throws Exception {
.withName("Test collection")
.build();

Item item = ItemBuilder.createItem(context, collection)
item = ItemBuilder.createItem(context, collection)
.withTitle("Test item")
.build();

Expand Down Expand Up @@ -238,6 +244,104 @@ public void testOnBitstreamContentDownloadWithTooManyEvents() throws Exception {

}

@Test
public void testOnBitstreamContentDownloadDefaultBundleConfig() throws Exception {
context.turnOffAuthorisationSystem();
Bundle licenseBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.LICENSE_BUNDLE_NAME).build();
Bitstream license = BitstreamBuilder.createBitstream(context, licenseBundle,
toInputStream("License", defaultCharset())).build();
context.restoreAuthSystemState();

assertThat(getStoredEventsAsList(), empty());

String bitstreamUrl = "/api/core/bitstreams/" + bitstream.getID() + "/content";

downloadBitstreamContent("Postman", "123456", "REF");
downloadContent("Chrome", "ABCDEFG", "REF-1", license);

assertThat(getStoredEventsAsList(), hasSize(1));

List<GoogleAnalyticsEvent> storedEvents = getStoredEventsAsList();

assertThat(storedEvents, contains(
event("123456", "127.0.0.1", "Postman", "REF", bitstreamUrl, "Test item"))
);

googleAsyncEventListener.sendCollectedEvents();

assertThat(getStoredEventsAsList(), empty());

verify(firstGaClientMock).isAnalyticsKeySupported(ANALYTICS_KEY);
verify(secondGaClientMock).isAnalyticsKeySupported(ANALYTICS_KEY);
verify(secondGaClientMock).sendEvents(ANALYTICS_KEY, storedEvents);
verifyNoMoreInteractions(firstGaClientMock, secondGaClientMock);
}

@Test
public void testOnBitstreamContentDownloadMultipleBundleConfig() throws Exception {
configurationService.setProperty("google-analytics.bundles",
List.of(Constants.DEFAULT_BUNDLE_NAME, "CONTENT"));

context.turnOffAuthorisationSystem();
Bundle contentBundle = BundleBuilder.createBundle(context, item).withName("CONTENT").build();
Bitstream content = BitstreamBuilder.createBitstream(context, contentBundle,
toInputStream("Test Content", defaultCharset())).build();
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item).withName("THUMBNAIL").build();
Bitstream thumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle,
toInputStream("Thumbnail", defaultCharset())).build();
context.restoreAuthSystemState();

assertThat(getStoredEventsAsList(), empty());

String bitstreamUrl = "/api/core/bitstreams/" + bitstream.getID() + "/content";
String contentUrl = "/api/core/bitstreams/" + content.getID() + "/content";

downloadBitstreamContent("Postman", "123456", "REF");
downloadContent("Chrome", "ABCDEFG", "REF-1", content);
downloadContent("Chrome", "987654", "REF-2", thumbnail);

assertThat(getStoredEventsAsList(), hasSize(2));

List<GoogleAnalyticsEvent> storedEvents = getStoredEventsAsList();

assertThat(storedEvents, contains(
event("123456", "127.0.0.1", "Postman", "REF", bitstreamUrl, "Test item"),
event("ABCDEFG", "127.0.0.1", "Chrome", "REF-1", contentUrl, "Test item")
));

googleAsyncEventListener.sendCollectedEvents();

assertThat(getStoredEventsAsList(), empty());

verify(firstGaClientMock).isAnalyticsKeySupported(ANALYTICS_KEY);
verify(secondGaClientMock).isAnalyticsKeySupported(ANALYTICS_KEY);
verify(secondGaClientMock).sendEvents(ANALYTICS_KEY, storedEvents);
verifyNoMoreInteractions(firstGaClientMock, secondGaClientMock);
}

@Test
public void testOnBitstreamContentDownloadNoneBundleConfig() throws Exception {
configurationService.setProperty("google-analytics.bundles", "none");

context.turnOffAuthorisationSystem();
Bundle contentBundle = BundleBuilder.createBundle(context, item).withName("CONTENT").build();
Bitstream content = BitstreamBuilder.createBitstream(context, contentBundle,
toInputStream("Test Content", defaultCharset())).build();
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item).withName("THUMBNAIL").build();
Bitstream thumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle,
toInputStream("Thumbnail", defaultCharset())).build();
context.restoreAuthSystemState();

assertThat(getStoredEventsAsList(), empty());

downloadBitstreamContent("Postman", "123456", "REF");
downloadContent("Chrome", "ABCDEFG", "REF-1", content);
downloadContent("Chrome", "987654", "REF-2", thumbnail);

assertThat(getStoredEventsAsList(), empty());
}

@SuppressWarnings("unchecked")
private List<GoogleAnalyticsEvent> getStoredEventsAsList() {
List<GoogleAnalyticsEvent> events = new ArrayList<>();
Expand All @@ -248,13 +352,18 @@ private List<GoogleAnalyticsEvent> getStoredEventsAsList() {
return events;
}

private void downloadBitstreamContent(String userAgent, String correlationId, String referrer) throws Exception {
private void downloadContent(String userAgent, String correlationId, String referrer, Bitstream bit)
throws Exception {
getClient(getAuthToken(admin.getEmail(), password))
.perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")
.header("USER-AGENT", userAgent)
.header("X-CORRELATION-ID", correlationId)
.header("X-REFERRER", referrer))
.perform(get("/api/core/bitstreams/" + bit.getID() + "/content")
.header("USER-AGENT", userAgent)
.header("X-CORRELATION-ID", correlationId)
.header("X-REFERRER", referrer))
.andExpect(status().isOk());
}

private void downloadBitstreamContent(String userAgent, String correlationId, String referrer) throws Exception {
downloadContent(userAgent, correlationId, referrer, bitstream);
}

}
9 changes: 9 additions & 0 deletions dspace/config/dspace.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,15 @@ log.report.dir = ${dspace.dir}/log
# For more details see https://developers.google.com/analytics/devguides/collection/protocol/ga4
# google.analytics.api-secret =

# Ensures only views of bitstreams in configured bundles result in a GA4 event.
# Config can contain multiple bundles for which the bitstream views will result in GA4 events, eg:
# google-analytics.bundles = ORIGINAL, CONTENT
# If config is not set or empty, the default fallback is Constants#CONTENT_BUNDLE_NAME bundle ('ORIGINAL').
# If config contains 'LICENSE' or 'THUMBNAIL' bundles, it may cause inflated bitstream view numbers.
# Set config to 'none' to disable GA4 bitstream events, eg:
# google-analytics.bundles = none
google-analytics.bundles = ORIGINAL

####################################################################
#---------------------------------------------------------------#
#----------------REQUEST ITEM CONFIGURATION---------------------#
Expand Down

0 comments on commit 2e1f74a

Please sign in to comment.