Skip to content

Commit

Permalink
Update Google Analytics to use GA4, fix #302
Browse files Browse the repository at this point in the history
  • Loading branch information
julgus committed Mar 3, 2023
1 parent 8d4f7c1 commit 03422fe
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> eventParameters = new HashMap<>();
final Map<String, String> userProperties = new HashMap<>();
eventParameters.put("app_version", this.version);

httpSend(eventName, eventParameters);
}

void httpSend(String eventName, final Map<String, String> 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
Expand Down Expand Up @@ -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<String, String> 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<String, String> userProperty) {
return String.format(" %s: {%n %s%n }", asElement(userProperty.getKey()), jsonElement(" ", "value", userProperty.getValue()));
}

static String renderMap(final Map<String, String> map, final Function<Map.Entry<String, String>, String> mapper) {
requireNonNull(map);
requireNonNull(mapper);
return map.entrySet().stream()
.map(mapper)
.collect(joining(String.format(",%n")));
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.speedment.jpastreamer.analytics.standard.internal.google;

final class InternalAnalyticsException extends RuntimeException {

InternalAnalyticsException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 03422fe

Please sign in to comment.