From d4bcc46a3570bcf3010d969a01bf3b639259a5fc Mon Sep 17 00:00:00 2001 From: jo-elimu <1451036+jo-elimu@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:43:54 +0700 Subject: [PATCH] feat(analytics): dashboard #1894 --- .../java/ai/elimu/util/AnalyticsHelper.java | 10 +++ .../analytics/MainAnalyticsController.java | 5 ++ ...VideoLearningEventCsvExportController.java | 90 +++++++++++++++++++ .../VideoLearningEventListController.java | 73 +++++++++++++++ .../webapp/WEB-INF/jsp/analytics/main.jsp | 2 +- .../analytics/video-learning-event/list.jsp | 88 ++++++++++++++++++ src/main/webapp/static/css/styles.css | 5 ++ .../ai/elimu/util/AnalyticsHelperTest.java | 6 ++ 8 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ai/elimu/web/analytics/VideoLearningEventCsvExportController.java create mode 100644 src/main/java/ai/elimu/web/analytics/VideoLearningEventListController.java diff --git a/src/main/java/ai/elimu/util/AnalyticsHelper.java b/src/main/java/ai/elimu/util/AnalyticsHelper.java index ebfcfbaaf..acc3e31f8 100644 --- a/src/main/java/ai/elimu/util/AnalyticsHelper.java +++ b/src/main/java/ai/elimu/util/AnalyticsHelper.java @@ -30,4 +30,14 @@ public static Integer extractVersionCodeFromCsvFilename(String filename) { Integer versionCode = Integer.valueOf(versionCodeAsString); return versionCode; } + + /** + * E.g. "7161a85a0e4751cd" --> "7161***51cd" + * + * @param androidId The Android ID + * @return The redacted version of the Android ID + */ + public static String redactAndroidId(String androidId) { + return androidId.substring(0, 4) + "***" + androidId.substring(12); + } } diff --git a/src/main/java/ai/elimu/web/analytics/MainAnalyticsController.java b/src/main/java/ai/elimu/web/analytics/MainAnalyticsController.java index 156d9546f..c722a65ce 100644 --- a/src/main/java/ai/elimu/web/analytics/MainAnalyticsController.java +++ b/src/main/java/ai/elimu/web/analytics/MainAnalyticsController.java @@ -2,6 +2,7 @@ import ai.elimu.dao.LetterLearningEventDao; import ai.elimu.dao.StoryBookLearningEventDao; +import ai.elimu.dao.VideoLearningEventDao; import ai.elimu.dao.WordLearningEventDao; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -26,6 +27,9 @@ public class MainAnalyticsController { @Autowired private StoryBookLearningEventDao storyBookLearningEventDao; + @Autowired + private VideoLearningEventDao videoLearningEventDao; + @RequestMapping(method = RequestMethod.GET) public String handleRequest(Model model) { logger.info("handleRequest"); @@ -33,6 +37,7 @@ public String handleRequest(Model model) { model.addAttribute("letterLearningEventCount", letterLearningEventDao.readCount()); model.addAttribute("wordLearningEventCount", wordLearningEventDao.readCount()); model.addAttribute("storyBookLearningEventCount", storyBookLearningEventDao.readCount()); + model.addAttribute("videoLearningEventCount", videoLearningEventDao.readCount()); return "analytics/main"; } diff --git a/src/main/java/ai/elimu/web/analytics/VideoLearningEventCsvExportController.java b/src/main/java/ai/elimu/web/analytics/VideoLearningEventCsvExportController.java new file mode 100644 index 000000000..f7c98d193 --- /dev/null +++ b/src/main/java/ai/elimu/web/analytics/VideoLearningEventCsvExportController.java @@ -0,0 +1,90 @@ +package ai.elimu.web.analytics; + +import ai.elimu.dao.VideoLearningEventDao; +import ai.elimu.model.analytics.VideoLearningEvent; +import ai.elimu.util.AnalyticsHelper; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.logging.log4j.LogManager; +import org.springframework.web.bind.annotation.RequestMethod; + +@Controller +@RequestMapping("/analytics/video-learning-event/list") +public class VideoLearningEventCsvExportController { + + private final Logger logger = LogManager.getLogger(); + + @Autowired + private VideoLearningEventDao videoLearningEventDao; + + @RequestMapping(value="/video-learning-events.csv", method = RequestMethod.GET) + public void handleRequest( + HttpServletResponse response, + OutputStream outputStream + ) throws IOException { + logger.info("handleRequest"); + + List videoLearningEvents = videoLearningEventDao.readAll(); + logger.info("videoLearningEvents.size(): " + videoLearningEvents.size()); + + CSVFormat csvFormat = CSVFormat.DEFAULT.builder() + .setHeader( + "id", // The Room database ID + "timestamp", + "android_id", + "package_name", + "video_id", + "video_title", + "learning_event_type", + "additional_data" + ) + .build(); + + StringWriter stringWriter = new StringWriter(); + CSVPrinter csvPrinter = new CSVPrinter(stringWriter, csvFormat); + + for (VideoLearningEvent videoLearningEvent : videoLearningEvents) { + logger.info("videoLearningEvent.getId(): " + videoLearningEvent.getId()); + + videoLearningEvent.setAndroidId(AnalyticsHelper.redactAndroidId(videoLearningEvent.getAndroidId())); + + csvPrinter.printRecord( + videoLearningEvent.getId(), + videoLearningEvent.getTimestamp().getTimeInMillis(), + videoLearningEvent.getAndroidId(), + videoLearningEvent.getPackageName(), + videoLearningEvent.getVideoId(), + videoLearningEvent.getVideoTitle(), + videoLearningEvent.getLearningEventType(), + videoLearningEvent.getAdditionalData() + ); + csvPrinter.flush(); + } + csvPrinter.close(); + + String csvFileContent = stringWriter.toString(); + + response.setContentType("text/csv"); + byte[] bytes = csvFileContent.getBytes(); + response.setContentLength(bytes.length); + try { + outputStream.write(bytes); + outputStream.flush(); + outputStream.close(); + } catch (IOException ex) { + logger.error(ex); + } + } +} diff --git a/src/main/java/ai/elimu/web/analytics/VideoLearningEventListController.java b/src/main/java/ai/elimu/web/analytics/VideoLearningEventListController.java new file mode 100644 index 000000000..7dba17bbc --- /dev/null +++ b/src/main/java/ai/elimu/web/analytics/VideoLearningEventListController.java @@ -0,0 +1,73 @@ +package ai.elimu.web.analytics; + +import ai.elimu.dao.VideoLearningEventDao; +import ai.elimu.model.analytics.VideoLearningEvent; +import ai.elimu.util.AnalyticsHelper; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@Controller +@RequestMapping("/analytics/video-learning-event/list") +public class VideoLearningEventListController { + + private final Logger logger = LogManager.getLogger(); + + @Autowired + private VideoLearningEventDao videoLearningEventDao; + + @RequestMapping(method = RequestMethod.GET) + public String handleRequest(Model model) { + logger.info("handleRequest"); + + List videoLearningEvents = videoLearningEventDao.readAll(); + for (VideoLearningEvent videoLearningEvent : videoLearningEvents) { + videoLearningEvent.setAndroidId(AnalyticsHelper.redactAndroidId(videoLearningEvent.getAndroidId())); + } + model.addAttribute("videoLearningEvents", videoLearningEvents); + + // Prepare chart data + List monthList = new ArrayList<>(); + List eventCountList = new ArrayList<>(); + if (!videoLearningEvents.isEmpty()) { + // Group event count by month (e.g. "Aug-2024", "Sep-2024") + Map eventCountByMonthMap = new HashMap<>(); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMM-yyyy"); + for (VideoLearningEvent event : videoLearningEvents) { + String eventMonth = simpleDateFormat.format(event.getTimestamp().getTime()); + eventCountByMonthMap.put(eventMonth, eventCountByMonthMap.getOrDefault(eventMonth, 0) + 1); + } + + // Iterate each month from 4 years ago until now + Calendar calendar4YearsAgo = Calendar.getInstance(); + calendar4YearsAgo.add(Calendar.YEAR, -4); + Calendar calendarNow = Calendar.getInstance(); + Calendar month = calendar4YearsAgo; + while (!month.after(calendarNow)) { + String monthAsString = simpleDateFormat.format(month.getTime()); + monthList.add(monthAsString); + + eventCountList.add(eventCountByMonthMap.getOrDefault(monthAsString, 0)); + + // Increase the date by 1 month + month.add(Calendar.MONTH, 1); + } + } + model.addAttribute("monthList", monthList); + model.addAttribute("eventCountList", eventCountList); + + return "analytics/video-learning-event/list"; + } +} diff --git a/src/main/webapp/WEB-INF/jsp/analytics/main.jsp b/src/main/webapp/WEB-INF/jsp/analytics/main.jsp index 5a6e8e7c9..2b54bb66b 100644 --- a/src/main/webapp/WEB-INF/jsp/analytics/main.jsp +++ b/src/main/webapp/WEB-INF/jsp/analytics/main.jsp @@ -47,7 +47,7 @@ movie Videos diff --git a/src/main/webapp/WEB-INF/jsp/analytics/video-learning-event/list.jsp b/src/main/webapp/WEB-INF/jsp/analytics/video-learning-event/list.jsp index e69de29bb..f81c5584d 100644 --- a/src/main/webapp/WEB-INF/jsp/analytics/video-learning-event/list.jsp +++ b/src/main/webapp/WEB-INF/jsp/analytics/video-learning-event/list.jsp @@ -0,0 +1,88 @@ + + VideoLearningEvents (${fn:length(videoLearningEvents)}) + + + +
+
+ + + +
+
+ +
+ + vertical_align_bottom + + + + + + + + + + + + + + + + + + + + + + +
timestampandroid_idpackage_namevideo_titlelearning_event_type
+ + + ${videoLearningEvent.androidId} + + ${videoLearningEvent.packageName} + + + + "${videoLearningEvent.video.title}" + + + "${videoLearningEvent.videoTitle}" + + + + ${videoLearningEvent.learningEventType} +
+
+
diff --git a/src/main/webapp/static/css/styles.css b/src/main/webapp/static/css/styles.css index 3c3441dc4..42a4c12bb 100644 --- a/src/main/webapp/static/css/styles.css +++ b/src/main/webapp/static/css/styles.css @@ -1,6 +1,11 @@ html { font-family: monospace, sans-serif; } +code { + background-color: rgba(103,58,183, 0.05); /* deep-purple */ + border-radius: 4px; + padding: 4px; +} body { background: #EEE; diff --git a/src/test/java/ai/elimu/util/AnalyticsHelperTest.java b/src/test/java/ai/elimu/util/AnalyticsHelperTest.java index 61481898d..d7ddfcc80 100644 --- a/src/test/java/ai/elimu/util/AnalyticsHelperTest.java +++ b/src/test/java/ai/elimu/util/AnalyticsHelperTest.java @@ -23,4 +23,10 @@ public void testExtractVersionCodeFromCsvFilename() { filename = "7161a85a0e4751cd_3001012_word-learning-events_2020-04-23.csv"; assertEquals(3001012, AnalyticsHelper.extractVersionCodeFromCsvFilename(filename)); } + + @Test + public void testRedactAndroidId() { + String androidId = "745f90e7aae26423"; + assertEquals("745f***6423", AnalyticsHelper.redactAndroidId(androidId)); + } }