X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fanalytics%2FGoogleAnalytics4.java;h=ba8a9209ead89218338fb254cd7231d75d902454;hb=d27e73b6519abfe5922ae716efe9a8dcc58c7751;hp=bf6f3f3c6c4dffb6579fe38cca00fa4920a325b5;hpb=deff57ef5fea596dda23366164478c4b41c34da9;p=jalview.git diff --git a/src/jalview/analytics/GoogleAnalytics4.java b/src/jalview/analytics/GoogleAnalytics4.java index bf6f3f3..ba8a920 100644 --- a/src/jalview/analytics/GoogleAnalytics4.java +++ b/src/jalview/analytics/GoogleAnalytics4.java @@ -2,6 +2,7 @@ package jalview.analytics; import java.io.IOException; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -10,12 +11,17 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Collections; +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 { @@ -33,6 +39,8 @@ public class GoogleAnalytics4 private static final String BASE_URL = "https://www.google-analytics.com/mp/collect"; + private static final String DESKTOP_EVENT = "desktop_event"; + private List> queryStringValues; private List> jsonObject; @@ -43,50 +51,130 @@ public class GoogleAnalytics4 private static boolean ENABLED = false; - public GoogleAnalytics4() + private static GoogleAnalytics4 instance = null; + + private static final Map defaultParams; + + static + { + defaultParams = new HashMap<>(); + defaultParams.put("app_name", + ChannelProperties.getProperty("app_name") + " Desktop"); + defaultParams.put("version", Cache.getProperty("VERSION")); + defaultParams.put("build_date", + Cache.getDefault("BUILD_DATE", "unknown")); + defaultParams.put("java_version", System.getProperty("java.version")); + String val = System.getProperty("sys.install4jVersion"); + if (val != null) + { + defaultParams.put("install4j_version", val); + } + val = System.getProperty("installer_template_version"); + if (val != null) + { + defaultParams.put("install4j_template_version", val); + } + val = System.getProperty("launcher_version"); + if (val != null) + { + defaultParams.put("launcher_version", val); + } + defaultParams.put("java_arch", + System.getProperty("os.arch") + " " + + System.getProperty("os.name") + " " + + System.getProperty("os.version")); + } + + private GoogleAnalytics4() { - this.reset(); + this.resetLists(); } - private static void setEnabled(boolean b) + public static void setEnabled(boolean b) { ENABLED = b; } - public void sendAnalytics() + public void sendAnalytics(String eventName, String... paramsStrings) { - sendAnalytics(null); + sendAnalytics(eventName, false, paramsStrings); } - public void sendAnalytics(String path) + /** + * The simplest way to send an analytic event. + * + * @param eventName + * The event name. To emulate a webpage view use "page_view" and set + * a "page_location" parameter. See + * https://developers.google.com/analytics/devguides/collection/ga4/events?client_type=gtag + * @param sendDefaultParams + * Flag whether to add the default params about the application. + * @param paramsStrings + * Optional multiple Strings in key, value pairs (there should be an + * even number of paramsStrings) to be set as parameters of the + * event. To emulate a webpage view use "page_location" as the URL in + * a "page_view" event. + */ + public void sendAnalytics(String eventName, boolean sendDefaultParams, + String... paramsStrings) { + // clear out old lists + this.resetLists(); + if (!ENABLED) { Console.debug("GoogleAnalytics4 not enabled."); return; } - if (path != null) + Map params = new HashMap<>(); + params.put("event_category", DESKTOP_EVENT); + params.put("event_label", eventName); + + // add these to all events from this application instance + if (sendDefaultParams) { - addEvent("page_event", path); + params.putAll(defaultParams); } + + // add (and overwrite with) the passed in params + if (paramsStrings != null && paramsStrings.length > 0) + { + 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 sb = new StringBuilder(); - sb.append(BASE_URL); - sb.append('?'); - sb.append(buildQueryString()); + StringBuilder urlSb = new StringBuilder(); + urlSb.append(BASE_URL); + urlSb.append('?'); + urlSb.append(buildQueryString()); try { - URL url = new URL(sb.toString()); + URL url = new URL(urlSb.toString()); URLConnection urlConnection = url.openConnection(); HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection; httpURLConnection.setRequestMethod("POST"); httpURLConnection.setDoOutput(true); - byte[] jsonBytes = buildJson().getBytes(StandardCharsets.UTF_8); + String jsonString = buildJson(); + + 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; httpURLConnection.setFixedLengthStreamingMode(jsonLength); @@ -127,20 +215,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); } @@ -181,7 +265,7 @@ public class GoogleAnalytics4 cookieValues.add(stringEntry(key, value)); } - public void reset() + private void resetLists() { jsonObject = new ArrayList<>(); events = new ArrayList(); @@ -189,6 +273,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(); @@ -198,10 +296,21 @@ public class GoogleAnalytics4 { sb.append('&'); } - sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); + try + { + sb.append(URLEncoder.encode(entry.getKey(), "UTF-8")); + } catch (UnsupportedEncodingException e) + { + sb.append(entry.getKey()); + } sb.append('='); - sb.append( - URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + try + { + sb.append(URLEncoder.encode(entry.getValue(), "UTF-8")); + } catch (UnsupportedEncodingException e) + { + sb.append(entry.getValue()); + } } return sb.toString(); } @@ -222,23 +331,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) @@ -249,66 +366,109 @@ 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)); + sb.append(String.join("", Collections.nCopies(repeat, whitespace))); + + } + 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)