From fecbdab721ea3f701b9c8107a5aac5371617943d Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Wed, 7 Jun 2023 14:39:49 +0100 Subject: [PATCH] JAL-4001 GA4 tracking is now ENABLED. Currently logging a page_view like the website with a schemeless, domainless path the same as the UA tracking, as well as a custom application_launch event with the path_location, separate app_name and version parameters --- src/jalview/analytics/GoogleAnalytics4.java | 205 +++++++++++++++++++-------- src/jalview/bin/Cache.java | 17 ++- src/jalview/bin/Console.java | 9 ++ src/jalview/gui/Desktop.java | 43 ++++-- src/jalview/gui/SplashScreen.java | 3 +- 5 files changed, 208 insertions(+), 69 deletions(-) diff --git a/src/jalview/analytics/GoogleAnalytics4.java b/src/jalview/analytics/GoogleAnalytics4.java index 3997543..bbbbee8 100644 --- a/src/jalview/analytics/GoogleAnalytics4.java +++ b/src/jalview/analytics/GoogleAnalytics4.java @@ -10,12 +10,16 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; +import jalview.bin.Cache; import jalview.bin.Console; +import jalview.util.ChannelProperties; public class GoogleAnalytics4 { @@ -43,37 +47,67 @@ public class GoogleAnalytics4 private static boolean ENABLED = false; - public GoogleAnalytics4() + private static GoogleAnalytics4 instance = null; + + private static final String appName; + + private static final String version; + + static { - this.reset(); + appName = ChannelProperties.getProperty("app_name") + " Desktop"; + version = Cache.getProperty("VERSION") + "_" + + Cache.getDefault("BUILD_DATE", "unknown"); } - private static void setEnabled(boolean b) + private GoogleAnalytics4() { - ENABLED = b; + this.resetLists(); } - public void sendAnalytics() + public static void setEnabled(boolean b) { - sendAnalytics(null); + ENABLED = b; } - public void sendAnalytics(String path) + public void sendAnalytics(String eventName, String... paramsStrings) { + // clear out old lists + this.resetLists(); + if (!ENABLED) { Console.debug("GoogleAnalytics4 not enabled."); return; } - if (path != null) + Map params = new HashMap<>(); + + // add these to all events from this application instance + params.put("app_name", appName); + params.put("version", version); + params.put("TEST", "you've got to pick a pocket or twoooooo"); + + // can be overwritten by passed in params + if (paramsStrings != null && paramsStrings.length > 0) { - addEvent("page_event", path); + if (paramsStrings.length % 2 != 0) + { + Console.warn( + "Cannot addEvent with odd number of paramsStrings. Ignoring the last one."); + } + for (int i = 0; i < paramsStrings.length - 1; i += 2) + { + String key = paramsStrings[i]; + String value = paramsStrings[i + 1]; + params.put(key, value); + } } + + addEvent(eventName, params); addQueryStringValue("measurement_id", MEASUREMENT_ID); addQueryStringValue("api_secret", API_SECRET); addJsonValue("client_id", CLIENT_ID); addJsonValues("events", Event.toObjectList(events)); - // addJsonValue("events", ) StringBuilder urlSb = new StringBuilder(); urlSb.append(BASE_URL); urlSb.append('?'); @@ -88,8 +122,8 @@ public class GoogleAnalytics4 String jsonString = buildJson(); - Console.debug("##### HTTP Request is: '" + urlSb.toString() + "'"); - Console.debug("##### POSTed JSON is:\n" + jsonString); + Console.debug("GA4: HTTP Request is: '" + urlSb.toString() + "'"); + Console.debug("GA4: POSTed JSON is:\n" + jsonString); byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8); int jsonLength = jsonBytes.length; @@ -132,20 +166,16 @@ public class GoogleAnalytics4 } } - public void addEvent(String name, String... paramsStrings) + public void addEvent(String name, Map params) { - if (paramsStrings.length % 2 != 0) - { - Console.error( - "Cannot addEvent with odd number of paramsStrings. Ignoring."); - return; - } Event event = new Event(name); - for (int i = 0; i < paramsStrings.length - 1; i += 2) + if (params != null && params.size() > 0) { - String key = paramsStrings[i]; - String value = paramsStrings[i + 1]; - event.addParam(key, value); + for (String key : params.keySet()) + { + String value = params.get(key); + event.addParam(key, value); + } } events.add(event); } @@ -186,7 +216,7 @@ public class GoogleAnalytics4 cookieValues.add(stringEntry(key, value)); } - public void reset() + private void resetLists() { jsonObject = new ArrayList<>(); events = new ArrayList(); @@ -194,6 +224,20 @@ public class GoogleAnalytics4 cookieValues = new ArrayList<>(); } + public static GoogleAnalytics4 getInstance() + { + if (instance == null) + { + instance = new GoogleAnalytics4(); + } + return instance; + } + + public static void reset() + { + getInstance().resetLists(); + } + private String buildQueryString() { StringBuilder sb = new StringBuilder(); @@ -227,23 +271,31 @@ public class GoogleAnalytics4 List> entries) { indent(sb, indent); - sb.append("{\n"); - for (Map.Entry entry : entries) + sb.append('{'); + newline(sb); + Iterator> entriesI = entries.iterator(); + while (entriesI.hasNext()) { + Map.Entry entry = entriesI.next(); String key = entry.getKey(); // TODO sensibly escape " characters in key Object value = entry.getValue(); - indent(sb, indent); - sb.append('"').append(key).append('"'); - sb.append(": "); - if (List.class.equals(value.getClass())) + indent(sb, indent + 1); + sb.append('"').append(quoteEscape(key)).append('"').append(':'); + space(sb); + if (value != null && value instanceof List) { - sb.append('\n'); + newline(sb); } - addJsonValue(sb, indent + 1, value); - sb.append(",\n"); + addJsonValue(sb, indent + 2, value); + if (entriesI.hasNext()) + { + sb.append(','); + } + newline(sb); } - sb.append("}\n"); + indent(sb, indent); + sb.append('}'); } private void addJsonValue(StringBuilder sb, int indent, Object value) @@ -254,66 +306,107 @@ public class GoogleAnalytics4 } try { - Class c = value.getClass(); - if (Map.Entry.class.equals(c)) + if (value instanceof Map.Entry) { Map.Entry entry = (Map.Entry) value; List> object = new ArrayList<>(); object.add(entry); - addJsonObject(sb, indent + 1, object); + addJsonObject(sb, indent, object); } - else if (List.class.equals(c)) + else if (value instanceof List) { // list of Map.Entries or list of values? List valueList = (List) value; - if (valueList.size() > 0 - && Map.Entry.class.equals(valueList.get(0).getClass())) + if (valueList.size() > 0 && valueList.get(0) instanceof Map.Entry) { // entries - indent(sb, indent); + // indent(sb, indent); List> entryList = (List>) value; - addJsonObject(sb, indent + 1, entryList); + addJsonObject(sb, indent, entryList); } else { // values indent(sb, indent); - sb.append("[\n"); - for (Object v : valueList) + sb.append('['); + newline(sb); + Iterator valueListI = valueList.iterator(); + while (valueListI.hasNext()) { - indent(sb, indent + 1); + Object v = valueListI.next(); addJsonValue(sb, indent + 1, v); - sb.append(",\n"); + if (valueListI.hasNext()) + { + sb.append(','); + } + newline(sb); } indent(sb, indent); sb.append("]"); } } - else if (String.class.equals(c)) + else if (value instanceof String) { - sb.append('"'); - sb.append((String) value); - sb.append('"'); + sb.append('"').append(quoteEscape((String) value)).append('"'); } - else if (Integer.class.equals(c)) + else if (value instanceof Integer) { sb.append(((Integer) value).toString()); } - else if (Boolean.class.equals(c)) + else if (value instanceof Boolean) { - sb.append(((Boolean) value).toString()); + sb.append('"').append(((Boolean) value).toString()).append('"'); } } catch (ClassCastException e) { Console.debug( - "Could not deal with type of jsonObject " + value.toString(), + "Could not deal with type of json Object " + value.toString(), e); } } - private void indent(StringBuilder sb, int indent) + private static String quoteEscape(String s) + { + if (s == null) + { + return null; + } + // this escapes quotation marks (") that aren't already escaped (in the + // string) ready to go into a quoted JSON string value + return s.replaceAll("((?= 0 && whitespace != null) + { + sb.append(whitespace.repeat(repeat)); + } + else + { + sb.append(whitespace); + } + } + + private static void indent(StringBuilder sb, int indent) + { + prettyWhitespace(sb, " ", indent); + } + + private static void newline(StringBuilder sb) + { + prettyWhitespace(sb, "\n", -1); + } + + private static void space(StringBuilder sb) { - sb.append(" ".repeat(indent)); + prettyWhitespace(sb, " ", -1); } protected static Map.Entry objectEntry(String s, Object o) diff --git a/src/jalview/bin/Cache.java b/src/jalview/bin/Cache.java index b03da07..63f2782 100755 --- a/src/jalview/bin/Cache.java +++ b/src/jalview/bin/Cache.java @@ -921,8 +921,6 @@ public class Cache protected static Class jgoogleanalyticstracker = null; - private static GoogleAnalytics4 gaTracker = null; - private static boolean useGA4 = true; /** @@ -932,14 +930,25 @@ public class Cache { if (useGA4) { + GoogleAnalytics4.setEnabled(true); + String appName = ChannelProperties.getProperty("app_name") + " Desktop"; String version = Cache.getProperty("VERSION") + "_" + Cache.getDefault("BUILD_DATE", "unknown"); String path = "/" + String.join("/", appName, version, APPLICATION_STARTED); - gaTracker = new GoogleAnalytics4(); - gaTracker.sendAnalytics(path); + GoogleAnalytics4 ga4 = GoogleAnalytics4.getInstance(); + + // This will add a page_view similar to the old UA analytics. + // We probably want to get rid of this once the application_launch event + // is being processed properly. + ga4.sendAnalytics("page_view", "page_location", path, "page_title", + APPLICATION_STARTED); + + // This will send a new "application_launch" event with parameters + // including the old-style "path", the channel name and version + ga4.sendAnalytics("application_launch", "page_location", path); } else { diff --git a/src/jalview/bin/Console.java b/src/jalview/bin/Console.java index 4b18484..30fd530 100644 --- a/src/jalview/bin/Console.java +++ b/src/jalview/bin/Console.java @@ -220,6 +220,11 @@ public class Console return JLogger.toLevel(level); } + public static JLogger getLogger() + { + return log; + } + public static boolean initLogger() { return initLogger(null); @@ -236,9 +241,13 @@ public class Console JLogger.LogLevel logLevel = JLogger.LogLevel.INFO; if (JLogger.isLevel(providedLogLevel)) + { logLevel = Console.getLogLevel(providedLogLevel); + } else + { logLevel = getCachedLogLevel(); + } if (!Platform.isJS()) { diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 6f2faae..44bf61e 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -535,6 +535,9 @@ public class Desktop extends jalview.jbgui.GDesktop setBounds(xPos, yPos, 900, 650); } + // start dialogue queue for single dialogues + startDialogQueue(); + if (!Platform.isJS()) /** * Java only @@ -3050,7 +3053,7 @@ public class Desktop extends jalview.jbgui.GDesktop /** * pause the queue */ - private java.util.concurrent.Semaphore block = new Semaphore(0); + private Semaphore block = new Semaphore(0); private static groovy.ui.Console groovyConsole; @@ -3068,12 +3071,7 @@ public class Desktop extends jalview.jbgui.GDesktop { if (dialogPause) { - try - { - block.acquire(); - } catch (InterruptedException x) - { - } + acquireDialogQueue(); } if (instance == null) { @@ -3091,12 +3089,41 @@ public class Desktop extends jalview.jbgui.GDesktop }); } + private boolean dialogQueueStarted = false; + public void startDialogQueue() { + if (dialogQueueStarted) + { + return; + } // set the flag so we don't pause waiting for another permit and semaphore // the current task to begin - dialogPause = false; + releaseDialogQueue(); + dialogQueueStarted = true; + } + + public void acquireDialogQueue() + { + try + { + block.acquire(); + dialogPause = true; + } catch (InterruptedException e) + { + jalview.bin.Console.debug("Interruption when acquiring DialogueQueue", + e); + } + } + + public void releaseDialogQueue() + { + if (!dialogPause) + { + return; + } block.release(); + dialogPause = false; } /** diff --git a/src/jalview/gui/SplashScreen.java b/src/jalview/gui/SplashScreen.java index 61273c7..0b7af0d 100755 --- a/src/jalview/gui/SplashScreen.java +++ b/src/jalview/gui/SplashScreen.java @@ -113,6 +113,7 @@ public class SplashScreen extends JPanel */ public SplashScreen(boolean isTransient) { + Desktop.instance.acquireDialogQueue(); this.transientDialog = isTransient; if (Platform.isJS()) // BH 2019 @@ -323,7 +324,7 @@ public class SplashScreen extends JPanel } closeSplash(); - Desktop.instance.startDialogQueue(); + Desktop.instance.releaseDialogQueue(); } /** -- 1.7.10.2