diff --git a/uPortal-events/src/main/java/org/apereo/portal/events/analytics/AnalyticsPortalEventsController.java b/uPortal-events/src/main/java/org/apereo/portal/events/analytics/AnalyticsPortalEventsController.java index 97dc1e904a0..daac3956d4a 100644 --- a/uPortal-events/src/main/java/org/apereo/portal/events/analytics/AnalyticsPortalEventsController.java +++ b/uPortal-events/src/main/java/org/apereo/portal/events/analytics/AnalyticsPortalEventsController.java @@ -14,6 +14,8 @@ */ package org.apereo.portal.events.analytics; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.text.ParseException; import java.util.Date; import java.util.HashMap; @@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -59,11 +62,29 @@ public ResponseEntity> getAnalyticsLevel() { return new ResponseEntity<>(response, HttpStatus.OK); } - @RequestMapping(method = RequestMethod.POST) - public ResponseEntity> postAnalytics( - @RequestBody Map analyticsData, HttpServletRequest request) { - service.publishEvent(request, analyticsData); - return new ResponseEntity<>(new HashMap<>(), HttpStatus.CREATED); + @RequestMapping( + method = RequestMethod.POST, + consumes = { + MediaType.APPLICATION_JSON_VALUE, + MediaType.APPLICATION_JSON_UTF8_VALUE, + MediaType.TEXT_PLAIN_VALUE + }) + public ResponseEntity postAnalytics( + @RequestBody String analyticsData, HttpServletRequest request) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + Map map = + objectMapper.readValue( + analyticsData, new TypeReference>() {}); + service.publishEvent(request, map); + return new ResponseEntity<>(new HashMap<>(), HttpStatus.CREATED); + } catch (Exception e) { + logger.warn("Failed to parse analytics data: " + e.getMessage()); + final ErrorResponse response = + new ErrorResponse( + "Post data was not in a JSON format, or the required attributes were not present."); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } } @PreAuthorize( diff --git a/uPortal-webapp/src/main/webapp/media/skins/respondr/common/common_skin.xml b/uPortal-webapp/src/main/webapp/media/skins/respondr/common/common_skin.xml index c2470ed8865..0fe223413fe 100644 --- a/uPortal-webapp/src/main/webapp/media/skins/respondr/common/common_skin.xml +++ b/uPortal-webapp/src/main/webapp/media/skins/respondr/common/common_skin.xml @@ -116,6 +116,8 @@ /rs/conflict-resolution/js/resolve-conflicts.js /rs/conflict-resolution/js/resolve-conflicts.min.js + /scripts/portal-analytics.js + ../../common/common_skin.xml diff --git a/uPortal-webapp/src/main/webapp/scripts/portal-analytics.js b/uPortal-webapp/src/main/webapp/scripts/portal-analytics.js new file mode 100644 index 00000000000..1886fadd373 --- /dev/null +++ b/uPortal-webapp/src/main/webapp/scripts/portal-analytics.js @@ -0,0 +1,79 @@ +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +"use strict"; + +// Wrapped in an IIFE to remove the global scope of the functions +(function () { + // Function that captures a click on an outbound link in Analytics. + var outboundClick = function(event) { + if ((event === undefined) || (event === null)) { + // Tried to process an outbound click, but there was no originating event + return; + } + + // Both path and composedPath need to be checked due to browser support + var anchorForEvent = (event.path || (event.composedPath && event.composedPath()))[0].closest('a'); + if ((anchorForEvent === undefined) || (anchorForEvent === null)) { + // Tried to process an outbound click, but there was no originating event anchor + return; + } + + if ((anchorForEvent.href === undefined) || + ( + anchorForEvent.href.startsWith('javascript') + )) { + // Not firing an analytic event due to href condition + return; + } + + var eventDetails = { + type: 'link', + url: anchorForEvent.href, + } + + fetch('/uPortal/api/analytics', { + keepalive: true, + method: 'POST', + body: JSON.stringify(eventDetails), + mode: 'cors', + credentials: 'same-origin', + headers: { + 'Content-Type': 'text/plain', + } + }) + .then(function(response) { console.log(response) }) + .catch(function(err) { console.log(err) }); + } + + var addPageLevelListeners = function() { + document.addEventListener('click', outboundClick); + document.addEventListener("beforeunload", function(event) { + document.removeEventListener('click', outboundClick); + }); + } + + window.onload = function() { + console.log( + 'Setting up Portal Analytics on links'); + var observer = new MutationObserver(addPageLevelListeners); + observer.observe(document.body, {attributeFilter: ["href"], childList: true, subtree: true}); + + addPageLevelListeners(); + } +})();