From 48536fa26d81ec9faff30d2d3b45ba446160816e Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Thu, 22 Jun 2023 15:52:52 +0100 Subject: [PATCH] JAL-4001 Added lookup to https://www.jalview.org/config/analytics/url to check API base url. Changed terminology to match Plausible API --- src/jalview/analytics/GoogleAnalytics4.java | 560 --------------------------- src/jalview/analytics/Plausible.java | 162 +++++--- 2 files changed, 101 insertions(+), 621 deletions(-) delete mode 100644 src/jalview/analytics/GoogleAnalytics4.java diff --git a/src/jalview/analytics/GoogleAnalytics4.java b/src/jalview/analytics/GoogleAnalytics4.java deleted file mode 100644 index 8666e31..0000000 --- a/src/jalview/analytics/GoogleAnalytics4.java +++ /dev/null @@ -1,560 +0,0 @@ -package jalview.analytics; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -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 jalview.bin.Cache; -import jalview.bin.Console; -import jalview.util.ChannelProperties; - -public class GoogleAnalytics4 -{ - private static final String JALVIEW_ID = "Jalview Desktop"; - - private static final String SESSION_ID = new Random().toString(); - - private static final String MEASUREMENT_ID = "G-6TMPHMXEQ0"; - - private static final String API_SECRET = "Qb9NSbqkRDqizG6j2BBJ2g"; - - // Client ID must be generated from gtag.js, used and captured. - // Will this affect geolocation? - private static final String CLIENT_ID = "2092672487.1686663096"; - - // set to true to use the GA4 measurement protocol validation service and - // print - // validation response. - // see - // https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag - private static boolean DEBUG = false; - - private static final String BASE_URL = "https://www.google-analytics.com/" - + (DEBUG ? "debug/" : "") + "mp/collect"; - - private static final String DESKTOP_EVENT = "desktop_event"; - - public static final String APPLICATION_BASE_URL = "https://www.jalview.org"; - - private List> queryStringValues; - - private List> jsonObject; - - private List events; - - private List> cookieValues; - - private static boolean ENABLED = false; - - 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.resetLists(); - } - - public static void setEnabled(boolean b) - { - ENABLED = b; - } - - public void sendAnalytics(String eventName, String... paramsStrings) - { - sendAnalytics(eventName, false, paramsStrings); - } - - /** - * 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; - } - 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) - { - 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); - addQueryStringValue("_geo", "1"); - addQueryStringValue("ep.anonymize_ip", "false"); - addJsonValue("client_id", CLIENT_ID); - addJsonValues("events", Event.toObjectList(events)); - StringBuilder urlSb = new StringBuilder(); - urlSb.append(BASE_URL); - urlSb.append('?'); - urlSb.append(buildQueryString()); - try - { - URL url = new URL(urlSb.toString()); - URLConnection urlConnection = url.openConnection(); - HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection; - httpURLConnection.setRequestMethod("POST"); - httpURLConnection.setDoOutput(true); - - 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); - httpURLConnection.setRequestProperty("Content-Type", - "application/json; charset=UTF-8"); - httpURLConnection.connect(); - try (OutputStream os = httpURLConnection.getOutputStream()) - { - os.write(jsonBytes); - } - int responseCode = httpURLConnection.getResponseCode(); - String responseMessage = httpURLConnection.getResponseMessage(); - - if (responseCode < 200 || responseCode > 299) - { - Console.warn("GoogleAnalytics4 connection failed: '" + responseCode - + " " + responseMessage + "'"); - } - else - { - Console.debug("GoogleAnalytics4 connection succeeded: '" - + responseCode + " " + responseMessage + "'"); - } - - if (DEBUG) - { - BufferedReader br = new BufferedReader(new InputStreamReader( - (httpURLConnection.getInputStream()))); - StringBuilder sb = new StringBuilder(); - String response; - while ((response = br.readLine()) != null) - { - sb.append(response); - } - String body = sb.toString(); - Console.debug("GoogleAnalytics4 response content; " + body); - } - } catch (MalformedURLException e) - { - Console.debug( - "Somehow the GoogleAnalytics4 BASE_URL and queryString is malformed: '" - + urlSb.toString() + "'", - e); - return; - } catch (IOException e) - { - Console.debug("Connection to GoogleAnalytics4 BASE_URL '" + BASE_URL - + "' failed.", e); - } catch (ClassCastException e) - { - Console.debug( - "Couldn't cast URLConnection to HttpURLConnection in GoogleAnalytics4.", - e); - } - } - - public void addEvent(String name, Map params) - { - Event event = new Event(name); - if (params != null && params.size() > 0) - { - for (String key : params.keySet()) - { - String value = params.get(key); - event.addParam(key, value); - } - } - events.add(event); - } - - private void addJsonObject(String key, - List> object) - { - jsonObject.add(objectEntry(key, object)); - } - - private void addJsonValues(String key, List values) - { - jsonObject.add(objectEntry(key, values)); - } - - private void addJsonValue(String key, String value) - { - jsonObject.add(objectEntry(key, value)); - } - - private void addJsonValue(String key, int value) - { - jsonObject.add(objectEntry(key, Integer.valueOf(value))); - } - - private void addJsonValue(String key, boolean value) - { - jsonObject.add(objectEntry(key, Boolean.valueOf(value))); - } - - private void addQueryStringValue(String key, String value) - { - queryStringValues.add(stringEntry(key, value)); - } - - private void addCookieValue(String key, String value) - { - cookieValues.add(stringEntry(key, value)); - } - - private void resetLists() - { - jsonObject = new ArrayList<>(); - events = new ArrayList(); - queryStringValues = new ArrayList<>(); - 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(); - for (Map.Entry entry : queryStringValues) - { - if (sb.length() > 0) - { - sb.append('&'); - } - try - { - sb.append(URLEncoder.encode(entry.getKey(), "UTF-8")); - } catch (UnsupportedEncodingException e) - { - sb.append(entry.getKey()); - } - sb.append('='); - try - { - sb.append(URLEncoder.encode(entry.getValue(), "UTF-8")); - } catch (UnsupportedEncodingException e) - { - sb.append(entry.getValue()); - } - } - return sb.toString(); - } - - private void buildCookieHeaders() - { - // TODO not needed yet - } - - private String buildJson() - { - StringBuilder sb = new StringBuilder(); - addJsonObject(sb, 0, jsonObject); - return sb.toString(); - } - - private void addJsonObject(StringBuilder sb, int indent, - List> entries) - { - indent(sb, indent); - 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 + 1); - sb.append('"').append(quoteEscape(key)).append('"').append(':'); - space(sb); - if (value != null && value instanceof List) - { - newline(sb); - } - addJsonValue(sb, indent + 2, value); - if (entriesI.hasNext()) - { - sb.append(','); - } - newline(sb); - } - indent(sb, indent); - sb.append('}'); - } - - private void addJsonValue(StringBuilder sb, int indent, Object value) - { - if (value == null) - { - return; - } - try - { - if (value instanceof Map.Entry) - { - Map.Entry entry = (Map.Entry) value; - List> object = new ArrayList<>(); - object.add(entry); - addJsonObject(sb, indent, object); - } - else if (value instanceof List) - { - // list of Map.Entries or list of values? - List valueList = (List) value; - if (valueList.size() > 0 && valueList.get(0) instanceof Map.Entry) - { - // entries - // indent(sb, indent); - List> entryList = (List>) value; - addJsonObject(sb, indent, entryList); - } - else - { - // values - indent(sb, indent); - sb.append('['); - newline(sb); - Iterator valueListI = valueList.iterator(); - while (valueListI.hasNext()) - { - Object v = valueListI.next(); - addJsonValue(sb, indent + 1, v); - if (valueListI.hasNext()) - { - sb.append(','); - } - newline(sb); - } - indent(sb, indent); - sb.append("]"); - } - } - else if (value instanceof String) - { - sb.append('"').append(quoteEscape((String) value)).append('"'); - } - else if (value instanceof Integer) - { - sb.append(((Integer) value).toString()); - } - else if (value instanceof Boolean) - { - sb.append('"').append(((Boolean) value).toString()).append('"'); - } - } catch (ClassCastException e) - { - Console.debug( - "Could not deal with type of json Object " + value.toString(), - e); - } - } - - 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) - { - prettyWhitespace(sb, " ", -1); - } - - protected static Map.Entry objectEntry(String s, Object o) - { - return new AbstractMap.SimpleEntry(s, o); - } - - protected static Map.Entry stringEntry(String s, String v) - { - return new AbstractMap.SimpleEntry(s, v); - } -} - -class Event -{ - private String name; - - private List> params; - - @SafeVarargs - public Event(String name, Map.Entry... paramEntries) - { - this.name = name; - this.params = new ArrayList>(); - for (Map.Entry paramEntry : paramEntries) - { - if (paramEntry == null) - { - continue; - } - params.add(paramEntry); - } - } - - public void addParam(String param, String value) - { - params.add(GoogleAnalytics4.stringEntry(param, value)); - } - - protected List> toObject() - { - List> object = new ArrayList<>(); - object.add(GoogleAnalytics4.objectEntry("name", (Object) name)); - if (params.size() > 0) - { - object.add(GoogleAnalytics4.objectEntry("params", (Object) params)); - } - return object; - } - - protected static List toObjectList(List events) - { - List eventObjectList = new ArrayList<>(); - for (Event event : events) - { - eventObjectList.add((Object) event.toObject()); - } - return eventObjectList; - } -} diff --git a/src/jalview/analytics/Plausible.java b/src/jalview/analytics/Plausible.java index f0ba59e..daf2d9a 100644 --- a/src/jalview/analytics/Plausible.java +++ b/src/jalview/analytics/Plausible.java @@ -35,7 +35,11 @@ public class Plausible private static final String DOMAIN = "jalview.org"; - private static final String API_BASE_URL = "https://plausible.io/api/event"; + private static final String CONFIG_API_BASE_URL = "https://www.jalview.org/config/analytics/url"; + + private static final String DEFAULT_API_BASE_URL = "https://DEFAULT.plausible.io/api/event"; + + private static final String API_BASE_URL; public static final String APPLICATION_BASE_URL = "desktop://localhost"; @@ -43,8 +47,6 @@ public class Plausible private List> jsonObject; - private List events; - private List> cookieValues; private static boolean ENABLED = false; @@ -53,36 +55,39 @@ public class Plausible private static Plausible instance = null; - private static final Map defaultParams; + private static final Map defaultProps; static { - defaultParams = new HashMap<>(); - defaultParams.put("app_name", + defaultProps = new HashMap<>(); + defaultProps.put("app_name", ChannelProperties.getProperty("app_name") + " Desktop"); - defaultParams.put("version", Cache.getProperty("VERSION")); - defaultParams.put("build_date", + defaultProps.put("version", Cache.getProperty("VERSION")); + defaultProps.put("build_date", Cache.getDefault("BUILD_DATE", "unknown")); - defaultParams.put("java_version", System.getProperty("java.version")); + defaultProps.put("java_version", System.getProperty("java.version")); String val = System.getProperty("sys.install4jVersion"); if (val != null) { - defaultParams.put("install4j_version", val); + defaultProps.put("install4j_version", val); } val = System.getProperty("installer_template_version"); if (val != null) { - defaultParams.put("install4j_template_version", val); + defaultProps.put("install4j_template_version", val); } val = System.getProperty("launcher_version"); if (val != null) { - defaultParams.put("launcher_version", val); + defaultProps.put("launcher_version", val); } - defaultParams.put("java_arch", + defaultProps.put("java_arch", System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version")); + + // ascertain the API_BASE_URL + API_BASE_URL = getAPIBaseURL(); } private Plausible() @@ -95,29 +100,28 @@ public class Plausible ENABLED = b; } - public void sendEvent(String eventName, String path, - String... paramsStrings) + public void sendEvent(String eventName, String urlString, + String... propsStrings) { - sendEvent(eventName, path, false, paramsStrings); + sendEvent(eventName, urlString, false, propsStrings); } /** * 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 + * The event name. To emulate a webpage view use "pageview" and set a + * "url" key/value. See https://plausible.io/docs/events-api + * @param sendDefaultProps + * Flag whether to add the default props about the application. + * @param propsStrings * 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. + * even number of propsStrings) to be set as property of the event. + * To emulate a webpage view set "url" as the URL in a "pageview" + * event. */ - public void sendEvent(String eventName, String path, - boolean sendDefaultParams, String... paramsStrings) + public void sendEvent(String eventName, String urlString, + boolean sendDefaultProps, String... propsStrings) { // clear out old lists this.resetLists(); @@ -127,45 +131,44 @@ public class Plausible Console.debug("Plausible not enabled."); return; } - Map params = new HashMap<>(); + Map props = new HashMap<>(); // add these to all events from this application instance - if (sendDefaultParams) + if (sendDefaultProps) { - params.putAll(defaultParams); + props.putAll(defaultProps); if (Jalview.isHeadlessMode()) { - params.put("headless", "true"); + props.put("headless", "true"); } } - // add (and overwrite with) the passed in params - if (paramsStrings != null && paramsStrings.length > 0) + // add (and overwrite with) the passed in props + if (propsStrings != null && propsStrings.length > 0) { - if (paramsStrings.length % 2 != 0) + if (propsStrings.length % 2 != 0) { Console.warn( - "Cannot addEvent with odd number of paramsStrings. Ignoring the last one."); + "Cannot addEvent with odd number of propsStrings. Ignoring the last one."); } - for (int i = 0; i < paramsStrings.length - 1; i += 2) + for (int i = 0; i < propsStrings.length - 1; i += 2) { - String key = paramsStrings[i]; - String value = paramsStrings[i + 1]; - params.put(key, value); + String key = propsStrings[i]; + String value = propsStrings[i + 1]; + props.put(key, value); } } addJsonValue("domain", DOMAIN); addJsonValue("name", eventName); - StringBuilder recordedUrlSb = new StringBuilder(APPLICATION_BASE_URL); - if (!APPLICATION_BASE_URL.endsWith("/") && !path.startsWith("/")) + StringBuilder eventUrlSb = new StringBuilder(APPLICATION_BASE_URL); + if (!APPLICATION_BASE_URL.endsWith("/") && !urlString.startsWith("/")) { - recordedUrlSb.append("/"); + eventUrlSb.append("/"); } - recordedUrlSb.append(path); - addJsonValue("url", recordedUrlSb.toString()); - addEvent(eventName, params); - addJsonObject("props", params); + eventUrlSb.append(urlString); + addJsonValue("url", eventUrlSb.toString()); + addJsonObject("props", props); StringBuilder urlSb = new StringBuilder(); urlSb.append(API_BASE_URL); String qs = buildQueryString(); @@ -250,20 +253,6 @@ public class Plausible } } - public void addEvent(String name, Map params) - { - Event event = new Event(name); - if (params != null && params.size() > 0) - { - for (String key : params.keySet()) - { - String value = params.get(key); - event.addParam(key, value); - } - } - events.add(event); - } - private void addJsonObject(String key, Map map) { List> list = new ArrayList<>(); @@ -314,7 +303,6 @@ public class Plausible private void resetLists() { jsonObject = new ArrayList<>(); - events = new ArrayList(); queryStringValues = new ArrayList<>(); cookieValues = new ArrayList<>(); } @@ -526,4 +514,56 @@ public class Plausible { return new AbstractMap.SimpleEntry(s, v); } + + private static String getAPIBaseURL() + { + try + { + URL url = new URL(CONFIG_API_BASE_URL); + URLConnection urlConnection = url.openConnection(); + HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection; + httpURLConnection.setRequestMethod("GET"); + httpURLConnection.setRequestProperty("User-Agent", USER_AGENT); + httpURLConnection.setConnectTimeout(5000); + httpURLConnection.setReadTimeout(3000); + httpURLConnection.connect(); + int responseCode = httpURLConnection.getResponseCode(); + String responseMessage = httpURLConnection.getResponseMessage(); + + if (responseCode < 200 || responseCode > 299) + { + Console.warn("Config URL connection to '" + CONFIG_API_BASE_URL + + "' failed: '" + responseCode + " " + responseMessage + + "'"); + } + + BufferedReader br = new BufferedReader( + new InputStreamReader((httpURLConnection.getInputStream()))); + StringBuilder sb = new StringBuilder(); + String response; + while ((response = br.readLine()) != null) + { + sb.append(response); + } + if (sb.length() > 7 && sb.substring(0, 5).equals("https")) + { + return sb.toString(); + } + + } catch (MalformedURLException e) + { + Console.debug("Somehow the config URL is malformed: '" + + CONFIG_API_BASE_URL + "'", e); + } catch (IOException e) + { + Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL + + "' failed.", e); + } catch (ClassCastException e) + { + Console.debug( + "Couldn't cast URLConnection to HttpURLConnection in Plausible.", + e); + } + return DEFAULT_API_BASE_URL; + } } \ No newline at end of file -- 1.7.10.2