From 03422fe5e5a1797019c66acabff651070ec65ac6 Mon Sep 17 00:00:00 2001 From: Julia Gustafsson Date: Fri, 3 Mar 2023 09:56:28 +0100 Subject: [PATCH] Update Google Analytics to use GA4, fix #302 --- .../standard/internal/google/EventType.java | 6 +- .../google/GoogleAnalyticsHandler.java | 116 +++++++++++------- .../standard/internal/google/HttpUtil.java | 93 ++++++++++++++ .../google/InternalAnalyticsException.java | 9 ++ .../standard/internal/google/JsonUtil.java | 38 ++++++ 5 files changed, 214 insertions(+), 48 deletions(-) create mode 100644 provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/HttpUtil.java create mode 100644 provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/InternalAnalyticsException.java create mode 100644 provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/JsonUtil.java diff --git a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/EventType.java b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/EventType.java index 41a6c8df3..d7a671c0e 100644 --- a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/EventType.java +++ b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/EventType.java @@ -16,9 +16,9 @@ enum EventType { - STARTED("jpastreamer-started", "start"), - ALIVE("jpastreamer-alive", null), - STOPPED("jpastreamer-stopped", "end"); + STARTED("started", "start"), + ALIVE("alive", null), + STOPPED("stopped", "end"); private final String eventName; private final String sessionControl; diff --git a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/GoogleAnalyticsHandler.java b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/GoogleAnalyticsHandler.java index 5117643d9..20ab14f2a 100644 --- a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/GoogleAnalyticsHandler.java +++ b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/GoogleAnalyticsHandler.java @@ -22,18 +22,21 @@ import java.nio.file.Paths; import java.security.SecureRandom; import java.util.*; +import java.util.function.Function; import java.util.stream.Stream; -import static com.speedment.common.rest.Param.param; -import static com.speedment.common.rest.Rest.encode; -import static java.lang.String.format; +import static com.speedment.jpastreamer.analytics.standard.internal.google.HttpUtil.urlEncode; +import static com.speedment.jpastreamer.analytics.standard.internal.google.JsonUtil.asElement; +import static com.speedment.jpastreamer.analytics.standard.internal.google.JsonUtil.jsonElement; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; public final class GoogleAnalyticsHandler implements Handler { private static final String COOKIE_FILE_NAME = "JPAstreamer.clientid"; - private static final String URL_STRING = "www.google-analytics.com"; - private static final String TRACKING_ID = "UA-54384165-3"; + private static final String URL_STRING = "https://www.google-analytics.com/mp/collect"; + private static final String MEASUREMENT_ID = "G-LNCF0RTS4N"; // JPAStreamer App Measurement ID + private static final String API_SECRET = "J-EHimWhT8anCwaHfq-h-Q"; private final String version; private final boolean demoMode; @@ -68,46 +71,19 @@ private void report(final EventType eventType) { requireNonNull(eventType); final String eventName = eventType.eventName() + (demoMode ? "-demo" : ""); - - final StringJoiner payload = new StringJoiner("&") - .add("v=" + encode("1")) // version. See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#v - .add("ds=" + encode("speedment")) // data source. See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ds - .add("tid=" + encode(TRACKING_ID)) // - .add("cid=" + clientId) - //.add("uip=" + encode(event.getIpAddress())) - //.add("ua=" + encode(event.getUserAgent())) - .add("t=" + encode("screenview")) // Hit type - .add("ni=" + encode("1")) // None interactive flag - .add("cd=" + encode(eventName)) // Screen Name - .add("an=" + encode("jpastreamer")) // Application Name - .add("av=" + encode(version)); // Application version - //.add("cd1=" + encode(event.getAppId().toString())) - //.add("cd2=" + encode(event.getDatabases())) - //.add("cd3=" + encode(event.getComputerName().orElse("no-host-specified"))) - //.add("cd4=" + encode(event.getEmailAddress().orElse("no-mail-specified"))) - - eventType.sessionControl() - .ifPresent(sc -> payload.add("sc=" + sc)); // Session control - - - // System.out.println("Parameters: "+payload.toString()); - - analytics.post("collect", payload.toString(), - //header("User-Agent", event.getUserAgent()), - param("z", Integer.toString(random.nextInt())) - ).handle((res, ex) -> { - if (ex != null) { - System.err.println("Exception while sending usage statistics to Google Analytics."); - ex.printStackTrace(); - } else if (!res.success()) { - System.err.println("Exception while sending usage statistics to Google Analytics."); - System.err.println(format("Google Analytics returned %d: %s", - res.getStatus(), res.getText() - )); - } - - return res; - }); + final Map eventParameters = new HashMap<>(); + final Map userProperties = new HashMap<>(); + eventParameters.put("app_version", this.version); + + httpSend(eventName, eventParameters); + } + + void httpSend(String eventName, final Map eventParameters) { + requireNonNull(eventName); + requireNonNull(eventParameters); + final String url = URL_STRING + "?measurement_id=" + urlEncode(MEASUREMENT_ID) + "&api_secret=" + urlEncode(API_SECRET); + final String json = jsonFor(eventName, acquireClientId(), eventParameters); + HttpUtil.send(url, json); } // This tries to read clientId from a "cookie" file in the @@ -135,4 +111,54 @@ private String acquireClientId() { return clientId; } + static String jsonFor(final String eventName, + final String clientId) { + requireNonNull(eventName); + requireNonNull(clientId); + return Stream.of( + "{", + jsonElement(" ", "clientId", clientId) + ',', + jsonElement(" ", "userId", clientId) + ',', + jsonElement(" ", "nonPersonalizedAds", true) + ',', + ' ' + asElement("events") + ": [{", + jsonElement(" ", "name", eventName) + ',', + " " + asElement("params") + ": {}", + " }],", + ' ' + asElement("userProperties") + ": {}", + "}" + ).collect(joining(JsonUtil.nl())); + } + + static String jsonFor(final String eventName, + final String clientId, + final Map eventParameters) { + requireNonNull(eventName); + requireNonNull(clientId); + requireNonNull(eventParameters); + return Stream.of( + "{", + jsonElement(" ", "clientId", clientId) + ',', + jsonElement(" ", "userId", clientId) + ',', + jsonElement(" ", "nonPersonalizedAds", true) + ',', + ' ' + asElement("events") + ": [{", + jsonElement(" ", "name", eventName) + ',', + " " + asElement("params") + ": {", + renderMap(eventParameters, e -> jsonElement(" ", e.getKey(), e.getValue())), + " }", + " }]", + "}" + ).collect(joining(JsonUtil.nl())); + } + static String userProperty(final Map.Entry userProperty) { + return String.format(" %s: {%n %s%n }", asElement(userProperty.getKey()), jsonElement(" ", "value", userProperty.getValue())); + } + + static String renderMap(final Map map, final Function, String> mapper) { + requireNonNull(map); + requireNonNull(mapper); + return map.entrySet().stream() + .map(mapper) + .collect(joining(String.format(",%n"))); + } + } diff --git a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/HttpUtil.java b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/HttpUtil.java new file mode 100644 index 000000000..14280b508 --- /dev/null +++ b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/HttpUtil.java @@ -0,0 +1,93 @@ +package com.speedment.jpastreamer.analytics.standard.internal.google; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import static java.util.Objects.requireNonNull; + +final class HttpUtil { + + private static final int DEFAULT_TIME_OUT_MS = 2_000; + + private static final String THREAD_NAME = "chronicle~analytics~http~client"; + private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(runnable -> { + final Thread thread = new Thread(runnable, THREAD_NAME); + thread.setDaemon(true); + return thread; + }); + + private HttpUtil() { + } + + public static void send(final String urlString, + final String body) { + requireNonNull(urlString); + requireNonNull(body); + EXECUTOR.execute(new Sender(urlString, body)); + } + + static String urlEncode(final String s) { + requireNonNull(s); + try { + return URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + System.err.println("Exception while URL encoding statistics for Google Analytics."); + e.printStackTrace(); + throw new InternalAnalyticsException("This should never happen as " + StandardCharsets.UTF_8 + " should always be present."); + } + } + + static final class Sender implements Runnable { + + private final String urlString; + private final String body; + + Sender(final String urlString, + final String body) { + requireNonNull(urlString); + requireNonNull(body); + this.urlString = urlString; + this.body = body; + } + + @Override + public void run() { + try { + final URL url = new URL(urlString); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // Do not linger if the connection is slow. Give up instead! + conn.setConnectTimeout(DEFAULT_TIME_OUT_MS); + conn.setReadTimeout(DEFAULT_TIME_OUT_MS); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "text/plain; charset=UTF-8"); + conn.setRequestProperty("t", "application/json"); + conn.setDoOutput(true); + try (OutputStream os = conn.getOutputStream()) { + final byte[] output = body.getBytes(StandardCharsets.UTF_8); + os.write(output, 0, output.length); + os.flush(); + } + + try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + final StringBuilder response = new StringBuilder(); + String sep = ""; + for (String responseLine; (responseLine = br.readLine()) != null; ) { + response.append(sep).append(responseLine); // preserve some white space + sep = " "; + } + final String logMsg = response.toString().replaceAll("\\s+(?=\\S)", " "); + if (!logMsg.isEmpty()) + System.out.println(logMsg); + } + + } catch (IOException ioe) { + System.err.println("Exception while sending usage statistics to Google Analytics."); + ioe.printStackTrace(); + } + } + } +} diff --git a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/InternalAnalyticsException.java b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/InternalAnalyticsException.java new file mode 100644 index 000000000..c4b6b7239 --- /dev/null +++ b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/InternalAnalyticsException.java @@ -0,0 +1,9 @@ +package com.speedment.jpastreamer.analytics.standard.internal.google; + +final class InternalAnalyticsException extends RuntimeException { + + InternalAnalyticsException(String message) { + super(message); + } + +} diff --git a/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/JsonUtil.java b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/JsonUtil.java new file mode 100644 index 000000000..1f27158d2 --- /dev/null +++ b/provider/analytics-standard/src/main/java/com/speedment/jpastreamer/analytics/standard/internal/google/JsonUtil.java @@ -0,0 +1,38 @@ +package com.speedment.jpastreamer.analytics.standard.internal.google; + +final class JsonUtil { + + private static final String NL = String.format("%n"); + + private JsonUtil() { + } + + static String jsonElement(final String indent, + final String key, + final Object value) { + return indent + asElement(key) + ": " + asElement(value); + } + + static String asElement(final Object value) { + return value instanceof CharSequence + ? '"' + escape(value.toString()) + '"' + : value.toString(); + + } + + static String escape(final String raw) { + return raw + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\b", "\\b") + .replace("\f", "\\f") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + // Todo: escape other non-printing characters ... + } + + static String nl() { + return NL; + } +}