Skip to content

Commit

Permalink
Feat/portal analytics frontend (#2588) (#2627)
Browse files Browse the repository at this point in the history
* Feat/portal analytics frontend (#2588)

* feat: initial structure of portal analytics

* WIP - addEventListeners on the page seem to work with web components

* WIP - cleaned up unneeded code

* Updates from PR review

* feat: portal analytics - tie in UX to API

* feat: portal-analytics: formatting

* feat: portal-analytics: format js

* chore: run goJF

Co-authored-by: Chris Beach <[email protected]>
  • Loading branch information
bjagg and cbeach47 authored Jan 20, 2023
1 parent 124b670 commit 3604725
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -59,11 +62,29 @@ public ResponseEntity<Map<String, String>> getAnalyticsLevel() {
return new ResponseEntity<>(response, HttpStatus.OK);
}

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<Map<String, String>> postAnalytics(
@RequestBody Map<String, Object> 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<String, Object> map =
objectMapper.readValue(
analyticsData, new TypeReference<Map<String, Object>>() {});
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
<js included="plain" resource="true">/rs/conflict-resolution/js/resolve-conflicts.js</js>
<js included="aggregated" resource="true">/rs/conflict-resolution/js/resolve-conflicts.min.js</js>

<js included="aggregated" resource="true">/scripts/portal-analytics.js</js>

<js import="true">../../common/common_skin.xml</js>

</resources>
79 changes: 79 additions & 0 deletions uPortal-webapp/src/main/webapp/scripts/portal-analytics.js
Original file line number Diff line number Diff line change
@@ -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();
}
})();

0 comments on commit 3604725

Please sign in to comment.