From 9519274a289d601b0bd279e1e490e3122ff27f50 Mon Sep 17 00:00:00 2001 From: Ed Lewis Date: Tue, 5 Dec 2017 10:09:07 +0000 Subject: [PATCH 001/128] Fix README.md formatting (close #190) --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ff2ac35a..86fd2c02 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Java Analytics for Snowplow -[ ![Build Status] [travis-image] ] [travis] [ ![Release] [release-image] ] [releases] [ ![License] [license-image] ] [license] +[![Build Status][travis-image]][travis] [![Release][release-image]][releases] [![License][license-image]][license] ## Overview -Add analytics to your Java software with the **[Snowplow] [snowplow]** event tracker for **[Java] [java]**. See also: **[Snowplow Android Tracker] [snowplow-android-tracker]**. +Add analytics to your Java software with the **[Snowplow][snowplow]** event tracker for **[Java][java]**. See also: **[Snowplow Android Tracker][snowplow-android-tracker]**. With this tracker you can collect event data from your Java-based desktop and server apps, servlets and games. Supports JDK7+. ## Quickstart -Assuming git, **[Vagrant] [vagrant-install]** and **[VirtualBox] [virtualbox-install]** installed: +Assuming git, **[Vagrant][vagrant-install]** and **[VirtualBox][virtualbox-install]** installed: ```bash host$ git clone https://github.com/snowplow/snowplow-java-tracker.git @@ -25,14 +25,14 @@ guest$ ./gradlew test | Technical Docs | Setup Guide | Roadmap | Contributing | |---------------------------------|---------------------------|-------------------------|-----------------------------------| -| ![i1] [techdocs-image] | ![i2] [setup-image] | ![i3] [roadmap-image] | ![i4] [contributing-image] | -| **[Technical Docs] [techdocs]** | **[Setup Guide] [setup]** | **[Roadmap] [roadmap]** | **[Contributing] [contributing]** | +| ![i1][techdocs-image] | ![i2][setup-image] | ![i3][roadmap-image] | ![i4][contributing-image] | +| **[Technical Docs][techdocs]** | **[Setup Guide][setup]** | **[Roadmap][roadmap]** | **[Contributing][contributing]** | ## Copyright and license The Snowplow Java Tracker is copyright 2014-2015 Snowplow Analytics Ltd. -Licensed under the **[Apache License, Version 2.0] [license]** (the "License"); +Licensed under the **[Apache License, Version 2.0][license]** (the "License"); you may not use this software except in compliance with the License. Unless required by applicable law or agreed to in writing, software From 8a0f7bf070e5fc4695e7b846a8ac2d7f72d5e2a5 Mon Sep 17 00:00:00 2001 From: Ed Lewis Date: Tue, 5 Dec 2017 11:49:54 +0000 Subject: [PATCH 002/128] Add simple-console sample project (close #191) --- examples/simple-console/.gitignore | 3 + examples/simple-console/README.md | 10 + examples/simple-console/build.gradle | 29 +++ .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/simple-console/gradlew | 172 ++++++++++++++++++ examples/simple-console/gradlew.bat | 84 +++++++++ .../main/java/com/snowplowanalytics/Main.java | 106 +++++++++++ .../java/com/snowplowanalytics/MainTest.java | 48 +++++ 8 files changed, 458 insertions(+) create mode 100644 examples/simple-console/.gitignore create mode 100644 examples/simple-console/README.md create mode 100644 examples/simple-console/build.gradle create mode 100644 examples/simple-console/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/simple-console/gradlew create mode 100644 examples/simple-console/gradlew.bat create mode 100644 examples/simple-console/src/main/java/com/snowplowanalytics/Main.java create mode 100644 examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java diff --git a/examples/simple-console/.gitignore b/examples/simple-console/.gitignore new file mode 100644 index 00000000..a7458c28 --- /dev/null +++ b/examples/simple-console/.gitignore @@ -0,0 +1,3 @@ +build/ +out/ + diff --git a/examples/simple-console/README.md b/examples/simple-console/README.md new file mode 100644 index 00000000..7dddded9 --- /dev/null +++ b/examples/simple-console/README.md @@ -0,0 +1,10 @@ +# Simple console sample + +This is a small Java console project that sends PageView events to a given collector. + +## Run + +``` +./gradlew jar + java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" +``` diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle new file mode 100644 index 00000000..3682679f --- /dev/null +++ b/examples/simple-console/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'java' +group = 'com.snowplowanalytics' +version = '0.0.1' + +repositories { + mavenCentral() + maven { + url "http://maven.snplow.com/releases" + } +} + +dependencies { + compile 'com.google.code.gson:gson:2.8+' + compile 'com.snowplowanalytics:snowplow-java-tracker:0.8.0' + testCompile 'junit:junit:4.12' +} + +task fatJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'simple-console', + 'Implementation-Version': version, + 'Main-Class': 'com.snowplowanalytics.Main' + } + baseName = project.name + '-all' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} + +tasks.jar.dependsOn(fatJar) diff --git a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..958d54cd --- /dev/null +++ b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Dec 05 10:27:05 GMT 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-bin.zip diff --git a/examples/simple-console/gradlew b/examples/simple-console/gradlew new file mode 100755 index 00000000..4453ccea --- /dev/null +++ b/examples/simple-console/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/simple-console/gradlew.bat b/examples/simple-console/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/examples/simple-console/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java new file mode 100644 index 00000000..2ab1ab48 --- /dev/null +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2017 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package com.snowplowanalytics; + +import com.snowplowanalytics.snowplow.tracker.DevicePlatform; +import com.snowplowanalytics.snowplow.tracker.Tracker; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; +import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; +import com.snowplowanalytics.snowplow.tracker.emitter.RequestCallback; +import com.snowplowanalytics.snowplow.tracker.events.PageView; +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; +import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.squareup.okhttp.OkHttpClient; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class Main { + + private static final int PAGEVIEW_COUNT = 20; + + public static String getUrlFromArgs(String[] args) { + if (args == null || args.length < 1) { + throw new IllegalArgumentException("Collector URL is required"); + } + return args[0]; + } + + public static HttpClientAdapter getClient(String url) { + // use okhttp to send events + OkHttpClient client = new OkHttpClient(); + + client.setConnectTimeout(5, TimeUnit.SECONDS); + client.setReadTimeout(5, TimeUnit.SECONDS); + client.setWriteTimeout(5, TimeUnit.SECONDS); + + return OkHttpClientAdapter.builder() + .url(url) + .httpClient(client) + .build(); + } + + public static void main(String[] args) { + String collectorEndpoint = getUrlFromArgs(args); + + System.out.println("Sending " + PAGEVIEW_COUNT + " events to " + collectorEndpoint); + + // get the client adapter + // this is used by the Java tracker to transmit events to the collector + HttpClientAdapter okHttpClientAdapter = getClient(collectorEndpoint); + + // the application id to attach to events + String appId = "java-tracker-sample-console-app"; + // the namespace to attach to events + String namespace = "demo"; + + // build an emitter, this is used by the tracker to batch and schedule transmission of events + Emitter emitter = BatchEmitter.builder() + .httpClientAdapter(okHttpClientAdapter) + .requestCallback(new RequestCallback() { + // let us know on successes (may be called multiple times) + @Override + public void onSuccess(int successCount) { + System.out.println("Successfully sent " + successCount + " events"); + } + + // let us know if something has gone wrong (may be called multiple times) + @Override + public void onFailure(int successCount, List failedEvents) { + System.err.println("Successfully sent " + successCount + " events; failed to send " + failedEvents.size() + " events"); + } + }) + .bufferSize(1) // send an event every time one is given (no batching). In production this number should be higher, depending on the size/event volume + .build(); + + // now we have the emitter, we need a tracker to turn our events into something a Snowplow collector can understand + Tracker tracker = new Tracker.TrackerBuilder(emitter, namespace, appId) + .base64(true) + .platform(DevicePlatform.ServerSideApp) + .build(); + + // This is a sample page view event, many other event types (such as self-describing events) are available + PageView pageViewEvent = PageView.builder() + .pageTitle("Hello world") + .pageUrl("http://helloworld.com") + .build(); + + for (int i = 0; i < PAGEVIEW_COUNT; i++) { + tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow + } + + } + +} diff --git a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java new file mode 100644 index 00000000..2a0d8730 --- /dev/null +++ b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package com.snowplowanalytics; + +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class MainTest { + + @Test + public void testGetUrl() { + String[] sample = {"com.acme", "world"}; + assertEquals("com.acme", Main.getUrlFromArgs(sample)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetUrlNull() { + Main.getUrlFromArgs(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetUrlEmpty() { + Main.getUrlFromArgs(new String[]{}); + } + + + @Test + public void testGetClientAdapter() { + HttpClientAdapter givenClient = Main.getClient("https://acme.com"); + assertNotNull(givenClient); + assertEquals("https://acme.com", givenClient.getUrl()); + } + +} \ No newline at end of file From 1d99daa285e5f870324cfcab94916c533117c97c Mon Sep 17 00:00:00 2001 From: Jin XIA Date: Tue, 5 Dec 2017 13:50:48 +0100 Subject: [PATCH 003/128] Make tracker exit cleanly (closes #187) --- .../snowplow/tracker/emitter/BatchEmitter.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 3a069be6..be738b59 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; // Google import com.google.common.base.Preconditions; @@ -38,6 +39,8 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); + private long closeTimeout = 5; + public static abstract class Builder> extends AbstractEmitter.Builder { private int bufferSize = 50; // Optional @@ -163,5 +166,18 @@ private SelfDescribingJson getFinalPost(List buffer) { @Override public void close() { flushBuffer(); + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(closeTimeout, TimeUnit.SECONDS)) { + executor.shutdownNow(); + if (!executor.awaitTermination(closeTimeout, TimeUnit.SECONDS)) + LOGGER.warn("Executor did not terminate"); + } + } catch (InterruptedException ie) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } } } From b8c055f09b8d3b65724413926ccf3e20ac276b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Renoux?= Date: Tue, 5 Dec 2017 13:52:59 +0100 Subject: [PATCH 004/128] Use UTF-8 encoding in events (closes #181) --- .../java/com/snowplowanalytics/snowplow/tracker/Utils.java | 5 +++-- .../snowplow/tracker/http/ApacheHttpClientAdapter.java | 3 ++- .../snowplow/tracker/payload/TrackerPayload.java | 3 ++- .../com/snowplowanalytics/snowplow/tracker/UtilsTest.java | 6 ++++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index b01d8583..49a76286 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -13,6 +13,7 @@ package com.snowplowanalytics.snowplow.tracker; // Java +import java.nio.charset.Charset; import java.util.*; import java.net.URL; import java.net.URLEncoder; @@ -108,8 +109,8 @@ public static String getTimezone() { * @param string the string too encode * @return a Base64 encoded string */ - public static String base64Encode(String string) { - return encodeBase64String(string.getBytes()); + public static String base64Encode(String string, Charset charset) { + return encodeBase64String(string.getBytes(charset)); } /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 3fa1c2c8..2cd7b279 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -23,6 +23,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -121,7 +122,7 @@ public int doPost(String url, String payload) { try { HttpPost httpPost = new HttpPost(url); httpPost.addHeader("Content-Type", Constants.POST_CONTENT_TYPE); - StringEntity params = new StringEntity(payload); + StringEntity params = new StringEntity(payload, ContentType.APPLICATION_JSON); httpPost.setEntity(params); HttpResponse httpResponse = httpClient.execute(httpPost); httpPost.releaseConnection(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 0a48f442..b581be42 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -13,6 +13,7 @@ package com.snowplowanalytics.snowplow.tracker.payload; // Java +import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; @@ -88,7 +89,7 @@ public void addMap(Map map, boolean base64Encoded, String typeEncoded, String ty LOGGER.info("Adding new map: {}", map); if (base64Encoded) { - add(typeEncoded, Utils.base64Encode(mapString)); + add(typeEncoded, Utils.base64Encode(mapString, StandardCharsets.UTF_8)); } else { add(typeNotEncoded, mapString); } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index c5bd629a..f1074e3f 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertTrue; // Java +import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; @@ -85,9 +86,10 @@ public void testGetTimezone() { @Test public void testBase64Encode() { - String expected = "aGVsbG93b3JsZHRlc3RiNjQ="; - String b64encoded = Utils.base64Encode("helloworldtestb64"); + String expected = "aGVsbG93b3JsZHRlc3RiNjR3aXRodXRmOGNoYXJzw7TDqcOgw6c="; + String b64encoded = Utils.base64Encode("helloworldtestb64withutf8charsôéàç", StandardCharsets.UTF_8); assertEquals(expected, b64encoded); + } @Test From 08c43239d59ac03aa1b2452699a8a03a75861d75 Mon Sep 17 00:00:00 2001 From: sowieso-fruehling Date: Mon, 5 Mar 2018 10:21:33 +0100 Subject: [PATCH 005/128] Close ResponseBody (closes #195) --- .../tracker/http/OkHttpClientAdapter.java | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 91b08a7a..11a157eb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -13,8 +13,7 @@ package com.snowplowanalytics.snowplow.tracker.http; // Java -import java.util.Iterator; -import java.util.Map; +import java.io.IOException; // Google import com.google.common.base.Preconditions; @@ -27,6 +26,7 @@ import com.squareup.okhttp.RequestBody; // Slf4j +import org.apache.http.HttpHeaders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,41 +96,70 @@ public Object getHttpClient() { * GET request to the configured endpoint. * * @param url the URL send - * @return the HttpResponse for the Request + * @return the HttpResponse code for the Request or -1 if exception is caught */ public int doGet(String url) { + + Response response = null; + int returnValue = -1; + Request request = new Request.Builder().url(url).build(); try { - Response response = httpClient.newCall(request).execute(); - return response.code(); + response = httpClient.newCall(request).execute(); + returnValue = response.code(); } catch (Exception e) { LOGGER.error("OkHttpClient GET Request failed: {}", e.getMessage()); - return -1; + } finally { + closeResponseBody(response); } + + return returnValue; } + /** * Attempts to send a group of payloads with a * POST request to the configured endpoint. * * @param url the URL to send to * @param payload the payload to send - * @return the HttpResponse for the Request + * @return the HttpResponse code for the Request or -1 if exception is caught */ public int doPost(String url, String payload) { + Response response = null; + int returnValue = -1; + try { RequestBody body = RequestBody.create(JSON, payload); Request request = new Request.Builder() .url(url) - .addHeader("Content-Type", Constants.POST_CONTENT_TYPE) + .addHeader(HttpHeaders.CONTENT_TYPE, Constants.POST_CONTENT_TYPE) .post(body) .build(); - Response response = httpClient.newCall(request).execute(); - return response.code(); + response = httpClient.newCall(request).execute(); + returnValue = response.code(); } catch (Exception e) { LOGGER.error("OkHttpClient POST Request failed: {}", e.getMessage()); - return -1; + } finally { + closeResponseBody(response); } + + return returnValue; + } + + + /** + * Closes response body as required by OkHttpClient documentation + * + * @param response OkHttpClient response + */ + private void closeResponseBody(Response response) { + if (response != null && response.body() != null) + try { + response.body().close(); + } catch (IOException e) { + LOGGER.error("OkHttpClient response body closing failed: {}", e.getMessage()); + } } -} +} \ No newline at end of file From 706edf9b64fac79fbcc3e73ff08885736af7595a Mon Sep 17 00:00:00 2001 From: Dimitar Nedev Date: Tue, 7 Aug 2018 10:30:32 +0200 Subject: [PATCH 006/128] Change `slf4j-simple` to a test runtime dependency (closes #188) --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a1c030da..1c867e49 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,8 @@ dependencies { optional 'com.squareup.okhttp:okhttp:2.2.0' // SLF4J logging API - compile 'org.slf4j:slf4j-simple:1.7.7' + compile 'org.slf4j:slf4j-api:1.7.7' + testRuntime 'org.slf4j:slf4j-simple:1.7.7' // Jackson JSON processor compile 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' From c638dfe659b28361b67d60c5e5e02319ad223379 Mon Sep 17 00:00:00 2001 From: "Michael R. Maletich" Date: Fri, 2 Nov 2018 14:12:04 -0500 Subject: [PATCH 007/128] Change some info statements to debug (close #202) --- .../snowplow/tracker/emitter/BatchEmitter.java | 2 +- .../snowplow/tracker/emitter/SimpleEmitter.java | 2 +- .../snowplow/tracker/payload/TrackerPayload.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index be738b59..c9d00f10 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -126,7 +126,7 @@ public void run() { LOGGER.error("BatchEmitter failed to send {} events: code: {}", buffer.size(), code); failure += buffer.size(); } else { - LOGGER.info("BatchEmitter successfully sent {} events: code: {}", buffer.size(), code); + LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", buffer.size(), code); success += buffer.size(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index a86e5d82..9493f678 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -89,7 +89,7 @@ public void run() { LOGGER.error("SimpleEmitter failed to send {} events: code: {}", 1, code); failure += 1; } else { - LOGGER.info("SimpleEmitter successfully sent {} events: code: {}", 1, code); + LOGGER.debug("SimpleEmitter successfully sent {} events: code: {}", 1, code); success += 1; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index b581be42..a1715ea4 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -47,7 +47,7 @@ public void add(String key, String value) { LOGGER.error("Invalid kv pair detected: {}->{}", key, value); return; } - LOGGER.info("Adding new kv pair: {}->{}", key, value); + LOGGER.debug("Adding new kv pair: {}->{}", key, value); payload.put(key, value); } @@ -63,7 +63,7 @@ public void addMap(Map map) { LOGGER.debug("Map passed in is null, returning without adding map."); return; } - LOGGER.info("Adding new map: {}", map); + LOGGER.debug("Adding new map: {}", map); for (Map.Entry entry : map.entrySet()) { add(entry.getKey(), entry.getValue()); } @@ -86,7 +86,7 @@ public void addMap(Map map, boolean base64Encoded, String typeEncoded, String ty } String mapString = Utils.mapToJSONString(map); - LOGGER.info("Adding new map: {}", map); + LOGGER.debug("Adding new map: {}", map); if (base64Encoded) { add(typeEncoded, Utils.base64Encode(mapString, StandardCharsets.UTF_8)); From a54200d0321a896d28c04574763d83cd829d3ab3 Mon Sep 17 00:00:00 2001 From: mhadam Date: Wed, 2 Jan 2019 11:43:20 -0500 Subject: [PATCH 008/128] Remove JDK7 and add OpenJDK8 in Travis build matrix (close #205) --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9094ac7a..4fa28150 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,5 @@ sudo: false language: java jdk: - - openjdk7 - - oraclejdk7 + - openjdk8 - oraclejdk8 From 42b4af20ff3a5d23630e71fe24de0af409b952da Mon Sep 17 00:00:00 2001 From: mhadam Date: Wed, 2 Jan 2019 11:45:43 -0500 Subject: [PATCH 009/128] Add Java 11 to Travis build matrix (close #207) --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4fa28150..52bafd8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,5 @@ language: java jdk: - openjdk8 - oraclejdk8 + - openjdk11 + - oraclejdk11 From 48bd5b902e7cb2023d5b87b4abd75f68c1acf8de Mon Sep 17 00:00:00 2001 From: mhadam Date: Fri, 28 Dec 2018 16:20:03 -0500 Subject: [PATCH 010/128] Upgrade Gradle to 5.0 (close #203) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 49896 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 78 +++++++++++---------- gradlew.bat | 84 +++++++++++++++++++++++ 5 files changed, 129 insertions(+), 38 deletions(-) create mode 100644 gradlew.bat diff --git a/build.gradle b/build.gradle index 1c867e49..60890724 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -wrapper.gradleVersion = '2.6' +wrapper.gradleVersion = '5.0' buildscript { repositories { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a8698b08ecc4158d828ca593c4928e9dd..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z zGh0A%o~bd$iYvP!egRY{hObDIvy_vXAOkeTgl5o!33m!l4VLm@<-FwT0+k|yl~vUh z@RFcL4=b(QQQmwQ;>FS_e96dyIU`jmR%&&Amxcb8^&?wvpK{_V_IbmqHh);$hBa~S z;^ph!k~noKv{`Ix7Hi&;Hq%y3wpqUsYO%HhI3Oe~HPmjnSTEasoU;Q_UfYbzd?Vv@ zD6ztDG|W|%xq)xqSx%bU1f>fF#;p9g=Hnjph>Pp$ZHaHS@-DkHw#H&vb1gARf4A*zm3Z75QQ6l( z=-MPMjish$J$0I49EEg^Ykw8IqSY`XkCP&TC?!7zmO`ILgJ9R{56s-ZY$f> zU9GwXt`(^0LGOD9@WoNFK0owGKDC1)QACY_r#@IuE2<`tep4B#I^(PRQ_-Fw(5nws zpkX=rVeVXzR;+%UzoNa;jjx<&@ABmU5X926KsQsz40o*{@47S2 z)p9z@lt=9?A2~!G*QqJWYT5z^CTeckRwhSWiC3h8PQ0M9R}_#QC+lz>`?kgy2DZio zz&2Ozo=yTXVf-?&E;_t`qY{Oy>?+7+I= zWl!tZM_YCLmGXY1nKbIHc;*Mag{Nzx-#yA{ zTATrWj;Nn;NWm6_1#0zy9SQiQV=38f(`DRgD|RxwggL(!^`}lcDTuL4RtLB2F5)lt z=mNMJN|1gcui=?#{NfL{r^nQY+_|N|6Gp5L^vRgt5&tZjSRIk{_*y<3^NrX6PTkze zD|*8!08ZVN)-72TA4Wo3B=+Rg1sc>SX9*X>a!rR~ntLVYeWF5MrLl zA&1L8oli@9ERY|geFokJq^O$2hEpVpIW8G>PPH0;=|7|#AQChL2Hz)4XtpAk zNrN2@Ju^8y&42HCvGddK3)r8FM?oM!3oeQ??bjoYjl$2^3|T7~s}_^835Q(&b>~3} z2kybqM_%CIKk1KSOuXDo@Y=OG2o!SL{Eb4H0-QCc+BwE8x6{rq9j$6EQUYK5a7JL! z`#NqLkDC^u0$R1Wh@%&;yj?39HRipTeiy6#+?5OF%pWyN{0+dVIf*7@T&}{v%_aC8 zCCD1xJ+^*uRsDT%lLxEUuiFqSnBZu`0yIFSv*ajhO^DNoi35o1**16bg1JB z{jl8@msjlAn3`qW{1^SIklxN^q#w|#gqFgkAZ4xtaoJN*u z{YUf|`W)RJfq)@6F&LfUxoMQz%@3SuEJHU;-YXb7a$%W=2RWu5;j44cMjC0oYy|1! zed@H>VQ!7=f~DVYkWT0nfQfAp*<@FZh{^;wmhr|K(D)i?fq9r2FEIatP=^0(s{f8GBn<8T zVz_@sKhbLE&d91L-?o`13zv6PNeK}O5dv>f{-`!ms#4U+JtPV=fgQ5;iNPl9Hf&9( zsJSm5iXIqN7|;I5M08MjUJ{J2@M3 zYN9ft?xIjx&{$K_>S%;Wfwf9N>#|ArVF^shFb9vS)v9Gm00m_%^wcLxe;gIx$7^xR zz$-JDB|>2tnGG@Rrt@R>O40AreXSU|kB3Bm)NILHlrcQ&jak^+~b`)2;otjI(n8A_X~kvp4N$+4|{8IIIv zw*(i}tt+)Kife9&xo-TyoPffGYe;D0a%!Uk(Nd^m?SvaF-gdAz4~-DTm3|Qzf%Pfd zC&tA;D2b4F@d23KV)Csxg6fyOD2>pLy#n+rU&KaQU*txfUj&D3aryVj!Lnz*;xHvl zzo}=X>kl0mBeSRXoZ^SeF94hlCU*cg+b}8p#>JZvWj8gh#66A0ODJ`AX>rubFqbBw z-WR3Z5`33S;7D5J8nq%Z^JqvZj^l)wZUX#7^q&*R+XVPln{wtnJ~;_WQzO{BIFV55 zLRuAKXu+A|7*2L*<_P${>0VdVjlC|n^@lRi}r?wnzQQm z3&h~C3!4C`w<92{?Dpea@5nLP2RJrxvCCBh%Tjobl2FupWZfayq_U$Q@L%$uEB6#X zrm_1TZA8FEtkd`tg)a_jaqnv3BC_O*AUq-*RNLOT)$>2D!r>FZdH&$x5G_FiAPaw4 zgK*7>(qd6R?+M3s@h>Z|H%7eGPxJWn_U$w`fb(Mp+_IK2Kj37YT#Xe5e6KS-_~mW} z`NXEovDJh7n!#q4b+=ne<7uB7Y2(TAR<3@PS&o3P$h#cZ-xF$~JiH6_gsv9v(#ehK zhSB_#AI%lF#+!MB5DMUN+Zhf}=t~{B|Fn{rGM?dOaSvX!D{oGXfS*%~g`W84JJAy4 zMdS?9Bb$vx?`91$J`pD-MGCTHNxU+SxLg&QY+*b_pk0R=A`F}jw$pN*BNM8`6Y=cm zgRh#vab$N$0=XjH6vMyTHQg*+1~gwOO9yhnzZx#e!1H#|Mr<`jJGetsM;$TnciSPJ z5I-R0)$)0r8ABy-2y&`2$33xx#%1mp+@1Vr|q_e=#t7YjjWXH#3F|Fu<G#+-tE2K7 zOJkYxNa74@UT_K4CyJ%mR9Yfa$l=z}lB(6)tZ1Ksp2bv$^OUn3Oed@=Q0M}imYTwX zQoO^_H7SKzf_#kPgKcs%r4BFUyAK9MzfYReHCd=l)YJEgPKq-^z3C%4lq%{&8c{2CGQ3jo!iD|wSEhZ# zjJoH87Rt{4*M_1GdBnBU3trC*hn@KCFABd=Zu`hK;@!TW`hp~;4Aac@24m|GI)Ula z4y%}ClnEu;AL4XVQ6^*!()W#P>BYC@K5mw7c4X|Hk^(mS9ZtfMsVLoPIiwI?w_X0- z#vyiV5q9(xq~fS`_FiUZw->8Awktga>2SrWyvZ|h@LVFtnY#T z%OX30{yiSov4!43kFd(8)cPRMyrN z={af_ONd;m=`^wc7lL|b7V!;zmCI}&8qz=?-6t=uOV;X>G{8pAwf9UJ`Hm=ubIbgR zs6bw3pFeQHL`1P1m5fP~fL*s?rX_|8%tB`Phrij^Nkj{o0oCo*g|ELexQU+2gt66=7}w5A+Qr}mHXC%)(ODT# zK#XTuzqOmMsO~*wgoYjDcy)P7G`5x7mYVB?DOXV^D3nN89P#?cp?A~c%c$#;+|10O z8z(C>mwk#A*LDlpv2~JXY_y_OLZ*Mt)>@gqKf-Ym+cZ{8d%+!1xNm3_xMygTp-!A5 zUTpYFd=!lz&4IFq)Ni7kxLYWhd0o2)ngenV-QP@VCu;147_Lo9f~=+=Nw$6=xyZzp zn7zAe41Sac>O60(dgwPd5a^umFVSH;<7vN>o;}YlMYhBZFZ}-sz`P^3oAI>SCZy&zUtwKSewH;CYysPQN7H>&m215&e2J? zY}>5N-LhaDeRF~C0cB>M z7@y&xh9q??*EIKnh*;1)n-WuSl6HkrI?OUiS^lx$Sr2C-jUm6zhd{nd(>#O8k9*kF zPom7-%w1NjFpj7WP=^!>Vx^6SG^r`r+M&s7V(uh~!T7aE;_ubqNSy)<5(Vi)-^Mp9 zEH@8Vs-+FEeJK%M0z3FzqjkXz$n~BzrtjQv`LagAMo>=?dO8-(af?k@UpL5J#;18~ zHCnWuB(m6G6a2gDq2s`^^5km@A3Rqg-oHZ68v5NqVc zHX_Iw!OOMhzS=gfR7k;K1gkEwuFs|MYTeNhc0js>Wo#^=wX4T<`p zR2$8p6%A9ZTac;OvA4u#Oe3(OUep%&QgqpR8-&{0gjRE()!Ikc?ClygFmGa(7Z^9X zWzmV0$<8Uh)#qaH1`2YCV4Zu6@~*c*bhtHXw~1I6q4I>{92Eq+ZS@_nSQU43bZyidk@hd$j-_iL=^^2CwPcaXnBP;s;b zA4C!k+~rg4U)}=bZ2q*)c4BZ#a&o!uJo*6hK3JRBhOOUQ6fQI;dU#3v>_#yi62&Sp z-%9JJxwIfQ`@w(_qH0J0z~(lbh`P zHoyp2?Oppx^WXwD<~20v!lYm~n53G1w*Ej z9^B*j@lrd>XGW43ff)F;5k|HnGGRu=wmZG9c~#%vDWQHlOIA9(;&TBr#yza{(?k0> zcGF&nOI}JhuPl`kLViBEd)~p2nY9QLdX42u9C~EUWsl-@CE;05y@^V1^wM$ z&zemD1oZd$Z))kEw9)_Mf+X#nT?}n({(+aXHK2S@j$MDsdrw-iLb?#r{?Vud?I5+I zVQ8U?LXsQ}8-)JBGaoawyOsTTK_f8~gFFJ&lhDLs8@Rw$ey-wr&eqSEU^~1jtHmz6 z!D2g4Yh?3VE*W8=*r&G`?u?M~AdO;uTRPfE(@=Gkg z7gh=EGu!6VJJ?S_>|5ZwY?dGFBp3B9m4J1=7u=HcGjsCW+y6`W?OWxfH?S#X8&Zk& zvz6tWcnaS1@~3FTH}q_*$)AjYA_j;yl0H0{I(CW7Rq|;5Q2>Ngd(tmJDp+~qHe_8y zPU_fiCrn!SJ3x&>o6;WDnjUVEt`2fhc9+uLI>99(l$(>Tzwpbh>O775OA5i`jaBdp zXnCwUgomyF3K$0tXzgQhSAc!6nhyRh_$fP}Rd$|*Y7?ah(JrN=I7+)+Hp4BLJJ2P~ zFD!)H^uR2*m7GQZpLUVS#R3^?2wCd}(gcFcz!u5KN9ldNJdh@%onf06z9m~T0n;dqg6@?>G@S|rPO*Kj>{su+R|7bH>osA&uD4eqxtr**k($ii`uO? z7-&VkiL4Rp3S&e+T}2Z#;NtWHZco(v8O3QMvN0g7l8GV|U2>x-DbamkZo5)bjaSFR zr~Y9(EvF9{o*@|nBPj+e5o$_K`%TH1hD=|its}|qS^o6EQu_gOuDUH=Dtzik;P7G$ zq%_T<>9O}bGIB?;IQ*H`BJ5NWF6+XLv@G7aZwcy(&BoepG~u`aIcG>y+;J7+L=wTZ zB=%n@O}=+mjBO%1lMo6C0@1*+mhBqqY((%QMUBhyeC~r*5WVqzisOXFncr*5Lr0q6 zyPU&NOV}Vt2jl>&yig4I6j93?D>Ft=keRh=Y;3*^Z-I26nkZ#Jj5OJ89_?@#9lNjp z#gfAO6i937)~I|98P%xAWxwmk(F&@lTMx63*FZ~2b{NHU+}EV8+kMAB0bM*Zn#&7ubt98!PT^ZcMOfwMgkYz6+;?CKbvV zQ}Z@s_3JcMPhF&y1?}9uZFIBiPR3g7lf=+XEr9Bl%zRfGcaKb*ZQq5b35ZkR@=JEw zP#iqgh2^#@VA-h)>r`7R-$1_ddGr&oWWV$rx;pkG0Yohp9p@In_p)hKvMo@qIv zcN2t{23&^Nj=Y&gX;*vJ;kjM zHE2`jtjVRRn;=WqVAY&m$z=IoKa{>DgJ;To@OPqNbh=#jiS$WE+O4TZIOv?niWs47 zQfRBG&WGmU~>2O{}h17wXGEnigSIhCkg%N~|e?hG8a- zG!Wv&NMu5z!*80>;c^G9h3n#e>SBt5JpCm0o-03o2u=@v^n+#6Q^r#96J5Q=Dd=>s z(n0{v%yj)=j_Je2`DoyT#yykulwTB+@ejCB{dA7VUnG>4`oE?GFV4sx$5;%9&}yxfz<-wWk|IlA|g&! zN_Emw#w*2GT=f95(%Y1#Viop;Yro3SqUrW~2`Fl?Ten{jAt==a>hx$0$zXN`^7>V_ zG*o7iqeZV)txtHUU2#SDTyU#@paP;_yxp!SAG##cB= zr@LoQg4f~Uy5QM++W`WlbNrDa*U;54`3$T;^YVNSHX4?%z|`B~i7W+kl0wBB`8|(l zAyI6dXL&-Sei0=f#P^m`z=JJ`=W;PPX18HF;5AaB%Zlze`#pz;t#7Bzq0;k8IyvdK=R zBW+4GhjOv+oNq^~#!5(+pDz)Ku{u60bVjyym8Or8L;iqR|qTcxEKTRm^Y%QjFYU=ab+^a|!{!hYc+= z%Qc02=prKpzD+jiiOwzyb(dELO|-iyWzizeLugO!<1(j|3cbR!8Ty1$C|l@cWoi?v zLe<5+(Z-eH++=fX**O-I8^ceYZgiA!!dH+7zfoP-Q+@$>;ab&~cLFg!uOUX7h0r== z`@*QP9tnV1cu1!9pHc43C!{3?-GUBJEzI(&#~vY9MEUcRNR*61)mo!RG>_Yb^rNN7 zR9^bI45V?3Lq`^^BMD!GONuO4NH#v9OP3@s%6*Ha3#S*;f z6JEi)qW#Iq#5BtIXT9Gby|H?NJG}DN#Li82kZ_Rt1=T0Z@U6OAdyf}4OD|Sk^2%-1 zzgvqZ@b6~kL!^sZLO$r{s!3fQ5bHW}8r$uTVS*iw1u8^9{YlPp_^Xm5IN zF|@)ZOReX zB*#tEbWEX~@f)ST|s$oUKS@drycE1tYtdJ9b*(uFTxNZ{n3BI*kF7wXgT6+@PI@vwH7iQS{1T!Nauk>fm8gOLe`->Pi~ z8)3=UL_$OLl2n7QZlHt846nkYFu4V};3LpYA%5VaF#a2#d2g0&ZO~3WA%1XlerVpg zCAlM;(9OqH@`(>Tha{*@R%twB!}1ng4V=^+R`Q{#fkRk)C|suozf-uCXrkIH2SC^C z6wlxR`yS;-U#uu#`OnD%U<41%C4mp>LYLPIbgVO~WsT1if)Y)T*8nUB`2*(B;U_ha1NWv2`GqrZ z3MWWpT3tZ!*N@d*!j3=@K4>X*gX4A^@QPAz24?7u90AXaLiFq=Z$|5p$Ok2|YCX_Z zFgNPiY2r_Bg2BQE!0z=_N*G?%0cNITmAru*!Mws=F+F&Qw!&1?DBN{vSy%IvGRV@1 zS->PARgL^XS!-aZj zi@`~LhWfD!H-L0kNv=Jil9zR0>jZLqu)cLq?$yXVyk%EteKcWbe^qh#spHJPa#?92 za(N(Kw0se^$7nQUQZBet;C_Dj5(2_?TdrXFYwmebq}YGQbN5Ex7M zGSCX~Ey;5AqAzEDNr%p^!cuG?&wIeY&Bm5guVg>8F=!nT%7QZTGR(uGM&IZuMw0V_ zhPiIFWm?H?aw*(v6#uVT@NEzi2h5I$cZ-n0~m$tmwdMTjG*of^Y%1 zW?Y%o*-_iMqEJhXo^!Qo?tGFUn1Mb|urN4_;a)9bila2}5rBS#hZ5wV+t1xbyF1TW zj+~cdjbcMgY$zTOq6;ODaxzNA@PZIXX(-=cT8DBd;9ihfqqtbDr9#gXGtK24BPxjZ z9+Xp>W1(s)->-}VX~BoQv$I|-CBdO`gULrvNL>;@*HvTdh@wyNf}~IB5mFnTitX2i z;>W>tlQyc2)T4Mq+f!(i3#KuK-I8Kj3Wm(UYx?KWWt8DEPR_Jdb9CE~Fjc7Rkh#gh zowNv()KRO@##-C+ig0l!^*ol!Bj%d32_N*~d!|&>{t!k3lc?6VrdlCCb1?qyoR42m zv;4KdwCgvMT*{?tJKa(T?cl|b;k4P>c&O@~g71K5@}ys$)?}WSxD;<5%4wEz7h=+q ztLumn6>leWdDk#*@{=v9p)MsvuJMyf_VEs;pJh?i3z7_W@Q|3p$a}P@MQ-NpMtDUBgH!h4Ia#L&POr4Qw0Tqdw^}gCmQAB z8Dgkzn?V!_@04(cx0~-pqJOpeP1_}@Ml3pCb45EJoghLows9ET13J8kt0;m$6-jO( z4F|p+JFD1NT%4bpn4?&)d+~<360$z5on`eS6{H`S>t`VS$>(D`#mC*XK6zULj1Da# zpV$gw$2Ui{07NiYJQQNK;rOepRxA>soNK~B2;>z;{Ovx`k}(dlOHHuNHfeR}7tmIp zcM}q4*Fq8vSNJYi@4-;}`@bC?nrUy`3jR%HXhs79qWI5;hyTpH5%n-NcKu&j(aGwT z1~{geeq?Jd>>HL+?2`0K8dB2pvTS=LO~tb~vx_<=iN8^rW!y@~lBTAaxHmvVQJSeJ z!cb9ffMdP1lgI=>QJN{XpM4{reRrdIt|v|0-8!p}M*Qw^uV1@Ho-YsNd0!a(os$F* zT0tGHA#0%u0j*%S>kL*73@~7|iP;;!JbWSTA@`#VHv_l_%Z7CgX@>dhg_ zgn0|U)SY~U-E5{QiT@(uPp#1jaz!(_3^Cbz2 z4ZgWWz=PdGCiGznk{^4TBfx_;ZjAHQ>dB4YI}zfEnTbf60lR%=@VWt0yc=fd38Ig* z)Q38#e9^+tA7K}IDG5Z~>JE?J+n%0_-|i2{E*$jb4h?|_^$HRHjVkiyX6@Y+)0C2a zA+eegpT1dUpqQFIwx;!ayQcWQBQTj1n5&h<%Lggt@&tE19Rm~Rijtqw6nmYip_xg0 zO_IYpU304embcWP+**H|Z5~%R*mqq+y{KbTVqugkb)JFSgjVljsR{-c>u+{?moCCl zTL)?85;LXk0HIDC3v*|bB-r_z%zvL6Dp__L*A~Z*o?$rm>cYux&)W=6#+Cb}TF&Kd zdCgz3(ZrNA>-V>$C{a^Y^2F!l_%3lFe$s(IOfLBLEJ4Mcd!y&Ah9r)7q?oc z5L(+S8{AhZ)@3bw0*8(}Xw{94Vmz6FrK&VFrJN;xB96QmqYEibFz|yHgUluA-=+yS}I-+#_Pk zN67-#8W(R^e7f!;i0tXbJgMmJZH%yEwn*-}5ew13D<_FYWnt?{Mv1+MI~u;FN~?~m z{hUnlD1|RkN}c1HQ6l@^WYbHAXPJ^m0te1woe;LDJ}XEJqh1tPf=sD0%b+OuR1aCoP>I>GBn4C24Zu$D)qg=gq;D??5 zUSj%;-Hvk_ffj-+SI{ZCp`gZcNu=L@_N}kCcs?TyMr-37fhy$?a<7lt1`fZw<%$8@B6(Wgo!#!z9z{ab|x`+&;kP!(gfdY}A-GP&4Cbh-S< z1(kmgnMyB2z3ipEj5;4<{(=&<7a>A_Jl`ujUKYV@%k(oD=cD7W@8~5O=R*zdjM_y; zXwme~0wo0aDa~9rDnjF=B}Bbj|DHRQjN|?@(F^=bVFdr!#mwr|c0843k>%~5J|7|v zSY=T)iPU6rEAwrM(xTZwPio%D4y9Z4kL0bMLKvu4yd)0ZJA3<;>a2q~rEfcREn}~1 zCJ~3c?Afvx?3^@+!lnf(kB6YwfsJ*u^y7kZA?VmM%nBmaMspWu?WXq4)jQsq`9EbT zlF2zJ)wXuAF*2u|yd5hNrG>~|i}R&ZyeetTQ!?Hz6xGZZb3W6|vR>Hq=}*m=V=Lsp zUOMxh;ZfP4za~C{Ppn^%rhitvpnu^G{Z#o-r?TdEgSbtK_+~_iD49xM;$}X*mJF02|WBL{SDqK9}p4N!G$3m=x#@T+4QcapM{4j|Q zwO!(hldpuSW#by!zHEP@tzIC|KdD z%BJzQ7Ho1(HemWm`Z8m_D#*`PZ-(R%sZmPrS$aHS#WPjH3EDitxN|DY+ zYC|3S?PQ3NNYau$Qk8f>{w}~xCX;;CE=7;Kp4^xXR8#&^L+y-jep7oO^wnQ840tg1 zuN17QKsfdqZPlB8OzwF+)q#IsmenEmIbRAJHJ$JjxzawKpk8^sBm3iy=*kB%LppNb zhSdk`^n?01FKQ;=iU+McN7Mk0^`KE>mMe1CQ2a_R26_}^$bogFm=2vqJake7x)KN( zYz;gRPL+r4*KD>1U+DU+1jh{mT8#P#(z9^(aDljpeN{mRmx{AZX&hXKXNuxj3x*RrpjvOaZ#`1EqK!$+8=0yv8}=;>f=E?5tGbRUd4%?QL zy$kq6mZeF%k6E1&8nwAYMd!-lRkhQTob$7s`*XqcHs;l~mHV}fx&0I&i!CHaPVSM{ zHdRh7a>hP)t@YTrWm9y zl-ENWSVzlKVvTdWK>)enmGCEw(WYS=FtY{srdE{Z(3~4svwd)ct;`6Y{^qiW+9E@A ztzd?lj5F#k`=E1U-n*1JJc0{x{0q!_tkD<_S6bGsW)^RxGu%Rj^Mvw|R0WP1SqvAI zs(MiAd@Y5x!UKu376&|quQNxir;{Iz(+}3k-GNb29HaQh?K30u=6sXpIc?j0hF{VY zM$Do*>pN)eRljAOgpx7fMfSrnZ7>fi@@>Jh;qxj1#-Vj}JC3E^GCbC(r55_AG>6cq z4ru34FtVuBt)bkX4>ZFWjToyu)VA>IE6hXc+^(3ruUaKRqHnx3z)(GXetm;^0D95s zQ&drwfjhM4*|q=;i5Io0eDf?I{p}qo@7i7abHX5qLu~VDwYf4bmV~-^M_U?DL(+cG z{AyE^a|*73Ft)o5k-p)+GLXj#q01VlJ9#ZJkf|+c%6qfRgVp&6NsU3~F?!uh}HJm73xq>v$h zYoW3wJE6n9P|;{8U<^%UE2wjR4x^G_Nc$J(i)!>;g4`CCh2z^Dth#ah#<`#axDR?F z4>~hnN2%B2ZUuU6j>m1Qjj~5jQSdA&Q#7hOky#=Ue)}7LPJ!8nbZO_0Sw{G>>M7&E zb1dy|0Zi$(ubk`4^XkVI%4WIpe?Bh!D~IjvZs14yHw=aQ8-`N-=P*?Kzi&eRGZ_6Z zT>eis`!Dy3eT3=vt#Lbc+;}i5XJf7zM3QneL{t?w=U<1rk7+z2Cu^|~=~54tAeSYF zsXHsU;nM0dpK>+71yo(NFLV-^Lf7%U?Q$*q{^j04Gl71ya2)^j`nmJ$cmI9eFMjp+ z#)jKmi4lZc<;l>!={@jTm%?!5jS;6;c*Ml55~r6Y?22B^K3bPhKQ(ICc&z%w<4W1= zjTTtz_}IA$%kCqU)h#$!Yq>>2mVG}qYL}!avmCWYV}x4!YEeq)pgTp| zR;+skHuc7YXRLrcbYXt>?@pa{l^2pL>RrZ!22zMmi1ZR?nkaWF*`@XFK4jGh&Em3vn(l z3~^Q9&tM^eV=f^lccCUc9v02z%^n5VV6s$~k0uq5B#Ipd6`M1Kptg^v<2jiNdlAWQ z_MmtNEaeYIHaiuaFQdG&df7miiB5lZkSbg&kxY*Eh|KTW`Tk~VwKC~+-GoYE+pvwc{+nIEizq6!xP>7ZQ(S2%48l$Y98L zvs7s<&0ArXqOb*GdLH0>Yq-f!{I~e~Z@FUIPm?jzqFZvz9VeZLYNGO}>Vh<=!Er7W zS!X6RF^et7)IM1pq57z*^hP5w7HKSDd8jHX!*gkKrGc-GssrNu5H%7-cNE{h$!aEQK3g*qy;= z)}pxO8;}nLVYm_24@iEs8)R7i;Th0n4->&$8m6(LKCRd(yn7KY%QHu_f=*#e`H^U( z{u!`9JaRD?Z?23fEXrjx>A@+a!y-_oaDB)o@2s{2%A97-ctFfrN0cXQ@6aGH`X~Nr z144?qk;MzDU-cgQOLfT3-ZR#hKmYtKG*iGf4ZJ`|`9!^SkBDUUSJCba)>mM!)k~(z zdjUqB`)~!UObMHB1b$UItM$<0kwlqHH;c z=)+~bkOcIT7vI0Iy(wD)vsg9|oi##%Rgrq`Ek;pN)}lbpz`iv{F4K*{ZZ?Zjixxxr zY|SPl2NsXH+5pimj+MvbZ_+HrfvdC13|9Zs)Y=nW$z<0mhl}%irBSm5T3ZrN#2AhY z_ZrTmS(L`U#y}VZ@~QL9wUS6AnU*7LWS02Xyz`b>%rTml#Wb0yr>@c(Ym*40g;P{V zjV1XSHdU>oY!&Jh7MzhzUV8(9E+yl5UJYga>=0Ldjwtc`5!1>LxaB-kVW;IlSPs+0 zUBx=m8OKVp<`frNvMK>WMO(iKY%PuvqD+PK*vP6f?_o!O)MCW5Ic zv(%f5PLHyOJ2h@Yn_to@54Yq;fdoy40&sbe3A$4uUXHsHP_~K}h#)p&TyOx(~JE?y(IBAQKl}~VQjVC-c6oZwmESL;`Xth?2)-b6ImNcJi z;w|`Q*k?`L(+Dp}t(FocvzWB(%~9$EAB6_J6CrA}hMj-Vy*6iA$FdV}!lvk%6}M)4 zTf<)EbXr9^hveAav1yA?>O0aNEpv0&rju{(Gt|dP=AP%)uQm~OE7@+wEhILrRLt&E zoEsF^nz>4yK1|EOU*kM+9317S;+bb7?TJM2UUpc!%sDp}7!<`i=W!ot8*C&fpj>mk#qt~GCeqcy)?W6sl>eUnR%yCBR&Ow-rc|q;lhnI+f-%`6Xf)% zIYZru;27%vA{Qi2=J`PQC<28;tFx(V^sgXf>)8WNxxQwT14M9I6- z+V0@tiCiDkv`7r-06sJS8@s|Lf>mV+8h}SPT4ZGPSMaFK7_SMXH$3KN7b2V?iV-jA zh1!Z>2tv^HVbHnNUAf-wQW#zMV(h8=3x2Swd|-%AczEIWLcm~EAu7rc3s%56b;7ME zj}$pe#fc^314Mb9i)xH^_#({)tTD4hsoz!7XcHUh9*G|}?k=D?9LBkTm2?fgaIG(%%$DL#}a-_990rQBU+M;jrf zCcvgM`+oyZmsUqc?lly9axZfO)02l$TMS#I+jHYY`Uk!gtDv|@GBQ||uaG^n*QR3Q z@tV?D;R;KmkxSDQh<2DkDC1?m?jTvf2i^T;+}aYhzL?ymNZmdns2e)}2V>tDCRw{= zTV3q3ZQDkdZQHi3?y{@8Y@1!SZQHi(y7|qSx$~Vl=iX<2`@y3eSYpsBV zI`Q-6;)B=p(ZbX55C*pu1C&yqS|@Pytis3$VDux0kxKK}2tO&GC;cH~759o?W2V)2 z)`;U(nCHBE!-maQz%z#zoRNpJR+GmJ!3N^@cA>0EGg?OtgM_h|j1X=!4N%!`g~%hdI3%yz&wq4rYChPIGnSg{H%i>96! z-(@qsCOfnz7ozXoUXzfzDmr>gg$5Z1DK$z#;wn9nnfJhy6T5-oi9fT^_CY%VrL?l} zGvnrMZP_P|XC$*}{V}b^|Hc38YaZQESOWqA1|tiXKtIxxiQ%Zthz?_wfx@<8I{XUW z+LH%eO9RxR_)8gia6-1>ZjZB2(=`?uuX|MkX082Dz*=ep%hMwK$TVTyr2*|gDy&QOWu zorR#*(SDS{S|DzOU$<-I#JTKxj#@0(__e&GRz4NuZZLUS8}$w+$QBgWMMaKge*2-) zrm62RUyB?YSUCWTiP_j-thgG>#(ZEN+~bMuqT~i3;Ri`l${s0OCvCM>sqtIX?Cy`8 zm)MRz-s^YOw>9`aR#J^tJz6$S-et%elmR2iuSqMd(gr6a#gA_+=N(I6%Cc+-mg$?_1>PlK zbgD2`hLZ?z4S~uhJf=rraLBL?H#c$cXyqt{u^?#2vX2sFb z^EU-9jmp{IZ~^ii@+7ogf!n_QawvItcLiC}w^$~vgEi(mX79UwDdBg`IlF42E5lWE zbSibqoIx*0>WWMT{Z_NadHkSg8{YW4*mZ@6!>VP>ey}2PuGwo%>W7FwVv7R!OD32n zW6ArEJX8g_aIxkbBl^YeTy5mhl1kFGI#n>%3hI>b(^`1uh}2+>kKJh0NUC|1&(l)D zh3Barl&yHRG+Le2#~u>KoY-#GSF>v)>xsEp%zgpq4;V6upzm3>V&yk^AD}uIF{vIn zRN-^d4(Sk6ioqcK@EObsAi#Z-u&Hh#kZdv1rjm4u=$2QF<6$mgJ4BE0yefFI zT7HWn?f668n!;x>!CrbdA~lDfjX?)315k1fMR~lG)|X_o()w|NX&iYUTKxI2TLl|r z{&TWcBxP>*;|XSZ1GkL&lSg?XL9rR4Ub&4&03kf};+6$F)%2rsI%9W_i_P|P%Z^b@ zDHH2LV*jB@Izq0~E4F^j04+C|SFiV8{!bth%bz(KfCg42^ zGz5P7xor$)I4VX}Cf6|DqZ$-hG7(}91tg#AknfMLFozF1-R~KS3&5I0GNb`P1+hIB z?OPmW8md3RB6v#N{4S5jm@$WTT{Sg{rVEs*)vA^CQLx?XrMKM@*gcB3mk@j#l0(~2 z9I=(Xh8)bcR(@8=&9sl1C?1}w(z+FA2`Z^NXw1t(!rpYH3(gf7&m=mm3+-sls8vRq z#E(Os4ZNSDdxRo&`NiRpo)Ai|7^GziBL6s@;1DZqlN@P_rfv4Ce1={V2BI~@(;N`A zMqjHDayBZ);7{j>)-eo~ZwBHz0eMGRu`43F`@I0g!%s~ANs>Vum~RicKT1sUXnL=gOG zDR`d=#>s?m+Af1fiaxYxSx{c5@u%@gvoHf#s6g>u57#@#a2~fNvb%uTYPfBoT_$~a^w96(}#d;-wELAoaiZCbM zxY4fKlS6-l1!b1!yra|`LOQoJB))=CxUAYqFcTDThhA?d}6FD$gYlk**!# zD=!KW>>tg1EtmSejwz{usaTPgyQm~o+NDg`MvNo)*2eWX*qAQ)4_I?Pl__?+UL>zU zvoT(dQ)pe9z1y}qa^fi-NawtuXXM>*o6Al~8~$6e>l*vX)3pB_2NFKR#2f&zqbDp7 z5aGX%gMYRH3R1Q3LS91k6-#2tzadzwbwGd{Z~z+fBD5iJ6bz4o1Rj#7cBL|x8k%jO z{cW0%iYUcCODdCIB(++gAsK(^OkY5tbWY;)>IeTp{{d~Y#hpaDa-5r#&Ha?+G{tn~ zb(#A1=WG1~q1*ReXb4CcR7gFcFK*I6Lr8bXLt9>9IybMR&%ZK15Pg4p_(v5Sya_70 ziuUYG@EBKKbKYLWbDZ)|jXpJJZ&bB|>%8bcJ7>l2>hXuf-h5Bm+ zHZ55e9(Sg>G@8a`P@3e2(YWbpKayoLQ}ar?bOh2hs89=v+ifONL~;q(d^X$7qfw=; zENCt`J*+G;dV_85dL3Tm5qz2K4m$dvUXh>H*6A@*)DSZ2og!!0GMoCPTbcd!h z@fRl3f;{F%##~e|?vw6>4VLOJXrgF2O{)k7={TiDIE=(Dq*Qy@oTM*zDr{&ElSiYM zp<=R4r36J69aTWU+R9Hfd$H5gWmJ?V){KU3!FGyE(^@i!wFjeZHzi@5dLM387u=ld zDuI1Y9aR$wW>s#I{2!yLDaVkbP0&*0Rw%6bi(LtieJQ4(1V!z!ec zxPd)Ro0iU%RP#L|_l?KE=8&DRHK>jyVOYvhGeH+Dg_E%lgA(HtS6e$v%D7I;JSA2x zJyAuin-tvpN9g7>R_VAk2y;z??3BAp?u`h-AVDA;hP#m+Ie`7qbROGh%_UTW#R8yfGp<`u zT0}L)#f%(XEE)^iXVkO8^cvjflS zqgCxM310)JQde*o>fUl#>ZVeKsgO|j#uKGi)nF_ur&_f+8#C0&TfHnfsLOL|l(2qn zzdv^wdTi|o>$q(G;+tkTKrC4rE)BY?U`NHrct*gVx&Fq2&`!3htkZEOfODxftr4Te zoseFuag=IL1Nmq45nu|G#!^@0vYG5IueVyabw#q#aMxI9byjs99WGL*y)AKSaV(zx z_`(}GNM*1y<}4H9wYYSFJyg9J)H?v((!TfFaWx(sU*fU823wPgN}sS|an>&UvI;9B(IW(V)zPBm!iHD} z#^w74Lpmu7Q-GzlVS%*T-z*?q9;ZE1rs0ART4jnba~>D}G#opcQ=0H)af6HcoRn+b z<2rB{evcd1C9+1D2J<8wZ*NxIgjZtv5GLmCgt?t)h#_#ke{c+R6mv6))J@*}Y25ef z&~LoA&qL-#o=tcfhjH{wqDJ;~-TG^?2bCf~s0k4Rr!xwz%Aef_LeAklxE=Yzv|3jf zgD0G~)e9wr@)BCjlY84wz?$NS8KC9I$wf(T&+79JjF#n?BTI)Oub%4wiOcqw+R`R_q<`dcuoF z%~hKeL&tDFFYqCY)LkC&5y(k7TTrD>35rIAx}tH4k!g9bwYVJ>Vdir4F$T*wC@$08 z9Vo*Q0>*RcvK##h>MGUhA9xix+?c1wc6xJhn)^9;@BE6i*Rl8VQdstnLOP1mq$2;!bfASHmiW7|=fA{k$rs^-8n{D6_ z!O0=_K}HvcZJLSOC6z-L^pl3Gg>8-rU#Sp1VHMqgXPE@9x&IHe;K3;!^SQLDP1Gk&szPtk| z!gP;D7|#y~yVQ?sOFiT*V(Z-}5w1H6Q_U5JM#iW16yZiFRP1Re z6d4#47#NzEm};1qRP9}1;S?AECZC5?6r)p;GIW%UGW3$tBN7WTlOy|7R1?%A<1!8Z zWcm5P6(|@=;*K&3_$9aiP>2C|H*~SEHl}qnF*32RcmCVYu#s!C?PGvhf1vgQ({MEQ z0-#j>--RMe{&5&$0wkE87$5Ic5_O3gm&0wuE-r3wCp?G1zA70H{;-u#8CM~=RwB~( zn~C`<6feUh$bdO1%&N3!qbu6nGRd5`MM1E_qrbKh-8UYp5Bn)+3H>W^BhAn;{BMii zQ6h=TvFrK)^wKK>Ii6gKj}shWFYof%+9iCj?ME4sR7F+EI)n8FL{{PKEFvB65==*@ ztYjjVTJCuAFf8I~yB-pN_PJtqH&j$`#<<`CruB zL=_u3WB~-;t3q)iNn0eU(mFTih<4nOAb>1#WtBpLi(I)^zeYIHtkMGXCMx+I zxn4BT0V=+JPzPeY=!gAL9H~Iu%!rH0-S@IcG%~=tB#6 z3?WE7GAfJ{>GE{?Cn3T!QE}GK9b*EdSJ02&x@t|}JrL{^wrM@w^&})o;&q816M5`} zv)GB;AU7`haa1_vGQ}a$!m-zkV(+M>q!vI0Swo18{;<>GYZw7-V-`G#FZ z;+`vsBihuCk1RFz1IPbPX8$W|nDk6yiU8Si40!zy{^nmv_P1=2H*j<^as01|W>BQS zU)H`NU*-*((5?rqp;kgu@+hDpJ;?p8CA1d65)bxtJikJal(bvzdGGk}O*hXz+<}J? zLcR+L2OeA7Hg4Ngrc@8htV!xzT1}8!;I6q4U&S$O9SdTrot<`XEF=(`1{T&NmQ>K7 zMhGtK9(g1p@`t)<)=eZjN8=Kn#0pC2gzXjXcadjHMc_pfV(@^3541)LC1fY~k2zn&2PdaW`RPEHoKW^(p_b=LxpW&kF?v&nzb z1`@60=JZj9zNXk(E6D5D}(@k4Oi@$e2^M%grhlEuRwVGjDDay$Qpj z`_X-Y_!4e-Y*GVgF==F0ow5MlTTAsnKR;h#b0TF>AyJe`6r|%==oiwd6xDy5ky6qQ z)}Rd0f)8xoNo)1jj59p;ChIv4Eo7z*{m2yXq6)lJrnziw9jn%Ez|A-2Xg4@1)ET2u zIX8`u5M4m=+-6?`S;?VDFJkEMf+=q?0D7?rRv)mH=gptBFJGuQo21rlIyP>%ymGWk z=PsJ>>q~i>EN~{zO0TklBIe(8i>xkd=+U@;C{SdQ`E03*KXmWm4v#DEJi_-F+3lrR z;0al0yXA&axWr)U%1VZ@(83WozZbaogIoGYpl!5vz@Tz5?u36m;N=*f0UY$ssXR!q zWj~U)qW9Q9Fg9UW?|XPnelikeqa9R^Gk77PgEyEqW$1j=P@L z*ndO!fwPeq_7J_H1Sx>#L$EO_;MfYj{lKuD8ZrUtgQLUUEhvaXA$)-<61v`C=qUhI zioV&KR#l50fn!-2VT`aMv|LycLOFPT{rRSRGTBMc)A`Cl%K&4KIgMf}G%Qpb2@cB* zw8obt-BI3q8Lab!O<#zeaz{P-lI2l`2@qrjD+Qy)^VKks5&SeT(I)i?&Kf59{F`Rw zuh7Q>SQNwqLO%cu2lzcJ7eR*3!g}U)9=EQ}js-q{d%h!wl6X3%H0Z2^8f&^H;yqti4z6TNWc& zDUU8YV(ZHA*34HHaj#C43PFZq7a>=PMmj4+?C4&l=Y-W1D#1VYvJ1~K%$&g-o*-heAgLXXIGRhU zufonwl1R<@Kc8dPKkb`i5P9VFT_NOiRA=#tM0WX2Zut)_ zLjAlJS1&nnrL8x8!o$G+*z|kmgv4DMjvfnvH)7s$X=-nQC3(eU!ioQwIkaXrl+58 z@v)uj$7>i`^#+Xu%21!F#AuX|6lD-uelN9ggShOX&ZIN+G#y5T0q+RL*(T(EP)(nP744-ML= z+Rs3|2`L4I;b=WHwvKX_AD56GU+z92_Q9D*P|HjPYa$yW0o|NO{>4B1Uvq!T;g_N- zAbNf%J0QBo1cL@iahigvWJ9~A4-glDJEK?>9*+GI6)I~UIWi>7ybj#%Po}yT6d6Li z^AGh(W{NJwz#a~Qs!IvGKjqYir%cY1+8(5lFgGvl(nhFHc7H2^A(P}yeOa_;%+bh` zcql{#E$kdu?yhRNS$iE@F8!9E5NISAlyeuOhRD)&xMf0gz^J927u5aK|P- z>B%*9vSHy?L_q)OD>4+P;^tz4T>d(rqGI7Qp@@@EQ-v9w-;n;7N05{)V4c7}&Y^!`kH3}Q z4RtMV6gAARY~y$hG7uSbU|4hRMn97Dv0$Le@1jDIq&DKy{D$FOjqw{NruxivljBGw zP4iM(4Nrz^^~;{QBD7TVrb6PB=B$<-e9!0QeE8lcZLdDeb?Gv$ePllO2jgy&FSbW* zSDjDUV^=`S(Oo0;k(Idvzh}aXkfO)F6AqB?wWqYJw-1wOn5!{-ghaHb^v|B^92LmQ9QZj zHA&X)fd%B$^+TQaM@FPXM$$DdW|Vl)4bM-#?Slb^qUX1`$Yh6Lhc4>9J$I4ba->f3 z9CeGO>T!W3w(){M{OJ+?9!MK68KovK#k9TSX#R?++W4A+N>W8nnk**6AB)e;rev=$ zN_+(?(YEX;vsZ{EkEGw%J#iJYgR8A}p+iW;c@V>Z1&K->wI>!x-+!0*pn|{f=XA7J zfjw88LeeJgs4YI?&dHkBL|PRX`ULOIZlnniTUgo-k`2O2RXx4FC76;K^|ZC6WOAEw zz~V0bZ29xe=!#Xk?*b{sjw+^8l0Koy+e7HjWXgmPa4sITz+$VP!YlJ$eyfi3^6gGx6jZLpbUzX;!Z6K}aoc!1CRi zB6Lhwt%-GMcUW;Yiy6Y7hX(2oksbsi;Z6k*=;y;1!taBcCNBXkhuVPTi+1N*z*}bf z`R=&hH*Ck5oWz>FR~>MO$3dbDSJ!y|wrff-H$y(5KadrA_PR|rR>jS=*9&J*ykWLr z-1Z^QOxE=!6I z%Bozo)mW7#2Hd$-`hzg=F@6*cNz^$#BbGlIf${ZV1ADc}sNl=B72g`41|F7JtZ^BT z+y}nqn3Ug`2scS_{MjykPW2~*k$i6PhvvxJCW;n!SK5B8Rpm41fCEdy=ea-4F`rN5 zF>ClKp#4?}pI7eR#6U|}t`DA!GQJB7nT$HVV*{qPjIRU1Ou3W;I^pCt54o|ZHvWaH zooFx9L%#yv)!P;^er5LCU$5@qXMhJ-*T5Ah8|}byGNU5oMp3V)yR;hWJKojJEregX z<1UPt%&~=5OuP(|B{ty);vLdoe7o^?`tkQa7zoXKAW6D@lc+FTzucotaOfJ!(Bm zHE8f8j@6||lH`y2<&hP}Q1wr(=6ze0D6NRL{7QaE1=nTAzqjIeD}Be&@#_d*dyurz z&L7xo-D9!dS`i>^GaIPArR@r=N#-ppIh!UBcb!N*?nLUO+*%C>_dCF1IH)q>5oT(t zjQo{AoDB;mWL;3&;vTt?;bvJSj>^Gq4Jrh}S}D>G)+b!>oRDWI?c_d77$kF5ms{Gx zak*>~*5AvaB-Xl)IgdZ^Cupv6HxQ0 zM(KPaDpPsPOd)e)aFw}|=tfzg@J1P8oJx2ZBY=g4>_G(Hkgld(u&~jN((eJ}5@b1} zI(P7j443AZj*I@%q!$JQ2?DZV47U!|Tt6_;tlb`mSP3 z74DE4#|1FMDqwYbT4P6#wSI%s?*wDc>)MR$4z9ZtJg04+CTUds>1JSDwI}=vpRoRR zLqx(Tvf34CvkTMOPkoH~$CG~fSZb;(2S4Q6Vpe9G83V={hwQ>acu+MCX)@0i>Vd`% z4I8Ye+7&Kcbh(*bN1etKmrpN)v|=eI+$oD=zzii6nP&w|kn2Y-f!(v<aE zKmOz#{6PZB(8zD={il`RO6D}v(@mN_66KXUAEefgg|;VmBfP?UrfB$&zaRw7oanna zkNmVGz4Vhd!vZSnp1(&_5^t;eSv6O771BloJAHi=Pnn+aa6y(e2iiE97uZ{evzQ^8 z*lN@ZYx<-hLXP^IuYLGf<01O*>nDp0fo;;Iyt`JADrxt7-jEF(vv_btyp6CT8=@5t zm`I0lW+2+_xj2CRL|40kcYysuyYeiGihGe&a)yilqP}5h+^)m8$=mzrUe`$(?BIY> zfF7-V10Gu0CkWF)wz04&hhI>es0NS7d`cnT`4y8K!wUAKv$H09fa>KeNQvwUNDT1zn}_*RHykC$CD%*h7vRCQ&Z z4&N-!L>(@8i?K$l5)13n0%VPPV`iG7Q$2{1T3JypLSvN%1kX73goBIOEmg=Uf$9e? zm}g>JFu}EQKH>|K!)m9teoCmTc`y2Ll}msZYyy0Pkqjeid66>DP_?C{KCw94lHvLW z-+X!2YSm70s833lH0o+|A%Xwsw`@8lE3ia0n_Dve;LC7@I+i~@%$lD|3fNf&R6ob6 z@iGfx^OC4s`$|vO!0jTWwVpX;X^EqJF{i324I>N=f@u+rTN+xJGGR0LsCQc;iFD=F zbZJrgOpS;04o^wP7HF5QBaJ$KJgS2V4u02ViWD=6+7rcu`uc&MOoyf%ZBU|gQZkUg z<}ax>*Fo?d*77Ia)+{(`X45{a8>Bi$u-0BWSteyp#GJnTs?&k&<0NeHA$Qb3;SAJK zl}H*~eyD-0qHI3SEcn`_7d zq@YRsFdBig+k490BZSQwW)j}~GvM7x>2ymO4zakaHZ!q6C2{fz^NvvD8+e%7?BQBH z-}%B{oROo2+|6g%#+XmyyIJrK_(uEbg%MHlBn3^!&hWi+9c0iqM69enep#5FvV_^r z?Yr(k*5FbG{==#CGI1zU0Wk{V?UGhBBfv9HP9A-AmcJmL^f4S zY3E2$WQa&n#WRQ5DOqty_Pu z-NWQGCR^Hnu^Vo2rm`-M>zzf|uMCUd1X0{wISJL2Pp=AO5 zF@(50!g|SYw3n<_VP0T~`WUjtY**6Npphr5bD%i3#*p7h8$#;XTLJAt5J-x~O1~`z z`2C~P4%XSI(JbrEmVMEwqdsa^aqXWg;A6KBn^jDxTl!}Q!^WhprL$kb(Iqq zUS`i$tIPs#hdE-zAaMGoxcG?Z;RO2L0Y|gcjV_)FFo|e)MtTl`msLTwq>po$`H6_U zhdWK97~M>idl9GE_WgobQkK_P85H_0jN?s3O)+m&68B`_;FnbZ3W*Qm++ghSs7|T4b7m~VVV%j0gl`Iw!?+-9#Lsb!j3O%fSTVuK z37V>qM81D+Atl};23`TqEAfEkQDpz$-1$e__>X2jN>xh@Sq)I6sj@< ziJ^66GSmW9c%F7eu6&_t$UaLXF4KweZecS1ZiHPWy-$e_7`jVk74OS*!z=l#(CQ^K zW-ke|g^&0o=hn+4uh-8lUh0>!VIXXnQXwKr>`94+2~<;+`k z$|}QZ>#pm2g}8k*;)`@EnM~ZQtci%_$ink9t6`HP{gn}P1==;WDAld3JX?k%^GcTU za>m|CH|UsyFhyJBwG5=`6562hkVRMQ=_ron-Vlm$4bG^GFz|Jh5mM{J1`!!hAr~8F^w> z^YhQ=c|bFn_6~9X$v(30v$5IX;#Nl-XXRPgs{g_~RS*znH^6Vhe}8>T?aMA|qfnWO zQpf(wr^PfygfM+m2u!9}F|frrZPBQ!dh(varsYo!tCV)WA(Wn^_t=WR_G7cQU`AGx zrK^B6<}9+$w;$vra)QWMKf_Tnqg93AMVZ6Qd=q6rdB{;ZhsoT zWy9QhnpEnc@Dauz4!8gq zqDanAX#$^vf-4~ZqUJtSe?SO+Hmb?)l2#}v(8}2+P{ZZuhlib0$3G0|a5?JR>QgUUP$HTE5hb`h>imq#7P+Y*-UVLm@9km|V# zoigziFt$bxgQMwqKKhd!c--&ciywIED>faY3zHLrA{V#IA)!mq!FXxf?1coGK~N(b zjwu*@2B1^(bzFVBJO`4EJ$=it!a0kbgUvPL;Er(0io{W4G7Bkqh)=g)uS|l0YfD}f zaCJwY7vR-D=P9M68`cmtmQ^!F-$lt@0S|9G7cHgT13A0xMv)HmH#Z<4{~iYo_VOD{ z5!kU+>mUOvHouw+-y?*cNlUlDwD#;6ZvAIc$YcwG&qKZFh>EtM(Eda+w)E$HcfZyB zG*$<*ae_ApE%gxWx%O^~XMnRSNLv!y`g99F(J_m)spJAc95P|_joOIoru%atbw z9PYgkcE*8x#)-W{>96KDl&74iW<#wrK)1s zxzU{`rW5af+dT6Z@_1dG<}CtDMT`EGVEXSL_5D9)Z;6UJe-TW7)M?bY%E;8G?Yc!$ zic;F5=#dba^P~7f#qvC}Nd#XEo2r_UlgfR_`B2^W0QjXU?RAi$>f&{G_Lu8Fp0qDp z?vAdm%z#3kcZmaJ@afooB=A@>8_N~O9Yzu=ZCEikM>UgU+{%>pPvmSNzGk@*jnc5~ z(Z#H4OL^gw>)gqZ!9X|3i4LAdp9vo)?F9QCR3##{BHoZ73Uk^Ha={2rc*TBijfKH- z=$cZQdc<5%*$kVo|{+bL3 zEoU&tq*YPR)^y-SISeQNQ)YZ9v>Hm4O=J)lf(y=Yu1ao&zj#5GVGxyj%V%vl9}dw< zO;@NRd4qe@Et}E@Q;SChBR2QPKll1{*5*jT*<$$5TywvC77vt=1=0xZ46>_17YzbiBoDffH(1_qFP7v2SVhZmA_7JDB50t#C39 z8V<9(E?bVWI<7d6MzcS^w!XmZ**{AO!~DZNU)pgr=yY1 zT@!AapE;yg&hmj*g{I3vd## zx+d%^O?d%%?Dba|l~X6ZOW|>FPsrjPjn-h4swysH!RNJUWofC?K(^0uHrBPrH5#W> zMn8^@USzjUucqo%+5&))Dnnw`5l1mp>roaA99Nkk4keZl2wAF7oa(!x?@8uGWzc5Q zM}g`}zf-D@B6lVFYWmmJ8a+_%z8g$C7Ww~PD9&jki08NY!b!fK288R;E?e3Z+Pk{is%HxQU`xu9+y5 zq?DWJD7kKp(B2J$t5Ij8-)?g!T9_n<&0L8F5-D0dp>9!Qnl#E{eDtkNo#lw6rMJG$ z9Gz_Z&a_6ie?;F1Y^6I$Mg9_sml@-z6t!YLr=ml<6{^U~UIbZUUa_zy>fBtR3Rpig zc1kLSJj!rEJILzL^uE1mQ}hjMCkA|ZlWVC9T-#=~ip%McP%6QscEGlYLuUxDUC=aX zCK@}@!_@~@z;70I+Hp5#Tq4h#d4r!$Np1KhXkAGlY$ap7IZ9DY})&(xoTyle8^dBXbQUhPE6ehWHrfMh&0=d<)E2+pxvWo=@`^ zIk@;-$}a4zJmK;rnaC)^a1_a_ie7OE*|hYEq1<6EG>r}!XI9+(j>oe!fVBG%7d}?U z#ja?T@`XO(;q~fe2CfFm-g8FbVD;O7y9c;J)k0>#q7z-%oMy4l+ zW>V~Y?s`NoXkBeHlXg&u*8B7)B%alfYcCriYwFQWeZ6Qre!4timF`d$=YN~_fPM5Kc8P;B-WIDrg^-j=|{Szq6(TC)oa!V7y zLmMFN1&0lM`+TC$7}on;!51{d^&M`UW ztI$U4S&}_R?G;2sI)g4)uS-t}sbnRoXVwM!&vi3GfYsU?fSI5Hn2GCOJ5IpPZ%Y#+ z=l@;;{XiY_r#^RJSr?s1) z4b@ve?p5(@YTD-<%79-%w)Iv@!Nf+6F4F1`&t~S{b4!B3fl-!~58a~Uj~d4-xRt`k zsmGHs$D~Wr&+DWK$cy07NH@_z(Ku8gdSN989efXqpreBSw$I%17RdxoE<5C^N&9sk!s2b9*#}#v@O@Hgm z2|U7Gs*@hu1JO$H(Mk)%buh~*>paY&Z|_AKf-?cz6jlT-v6 zF>l9?C6EBRpV2&c1~{1$VeSA|G7T(VqyzZr&G>vm87oBq2S%H0D+RbZm}Z`t5Hf$C zFn7X*;R_D^ z#Ug0tYczRP$s!6w<27;5Mw0QT3uNO5xY($|*-DoR1cq8H9l}_^O(=g5jLnbU5*SLx zGpjfy(NPyjL`^Oln_$uI6(aEh(iS4G=$%0;n39C(iw79RlXG>W&8;R1h;oVaODw2nw^v{~`j(1K8$ z5pHKrj2wJhMfw0Sos}kyOS48Dw_~=ka$0ZPb!9=_FhfOx9NpMxd80!a-$dKOmOGDW zi$G74Sd(-u8c!%35lL|GkyxZdlYUCML{V-Ovq{g}SXea9t`pYM^ioot&1_(85oVZ6 zUhCw#HkfCg7mRT3|>99{swr3FlA@_$RnE?714^o;vps4j4}u=PfUAd zMmV3j;Rogci^f!ms$Z;gqiy7>soQwo7clLNJ4=JAyrz;=*Yhe8q7*$Du970BXW89Xyq92M4GSkNS-6uVN~Y4r7iG>{OyW=R?@DmRoi9GS^QtbP zFy2DB`|uZTv8|ow|Jcz6?C=10U$*_l2oWiacRwyoLafS!EO%Lv8N-*U8V+2<_~eEA zgPG-klSM19k%(%;3YM|>F||hE4>7GMA(GaOvZBrE{$t|Hvg(C2^PEsi4+)w#P4jE2XDi2SBm1?6NiSkOp-IT<|r}L9)4tLI_KJ*GKhv16IV}An+Jyx z=Mk`vCXkt-qg|ah5=GD;g5gZQugsv!#)$@ zkE=6=6W9u9VWiGjr|MgyF<&XcKX&S3oN{c{jt-*1HHaQgY({yjZiWW97rha^TxZy< z2%-5X;0EBP>(Y9|x*603*Pz-eMF5*#4M;F`QjTBH>rrO$r3iz5 z?_nHysyjnizhZQMXo1gz7b{p`yZ8Q78^ zFJ3&CzM9fzAqb6ac}@00d*zjW`)TBzL=s$M`X*0{z8$pkd2@#4CGyKEhzqQR!7*Lo@mhw`yNEE6~+nF3p;Qp;x#-C)N5qQD)z#rmZ#)g*~Nk z)#HPdF_V$0wlJ4f3HFy&fTB#7Iq|HwGdd#P3k=p3dcpfCfn$O)C7;y;;J4Za_;+DEH%|8nKwnWcD zBgHX)JrDRqtn(hC+?fV5QVpv1^3=t2!q~AVwMBXohuW@6p`!h>>C58%sth4+Baw|u zh&>N1`t(FHKv(P+@nT$Mvcl){&d%Y5dx|&jkUxjpUO3ii1*^l$zCE*>59`AvAja%`Bfry-`?(Oo?5wY|b4YM0lC?*o7_G$QC~QwKslQTWac z#;%`sWIt8-mVa1|2KH=u!^ukn-3xyQcm4@|+Ra&~nNBi0F81BZT$XgH@$2h2wk2W% znpo1OZuQ1N>bX52II+lsnQ`WVUxmZ?4fR_f0243_m`mbc3`?iy*HBJI)p2 z`GQ{`uS;@;e1COn-vgE2D!>EheLBCF-+ok-x5X8Cu>4H}98dH^O(VlqQwE>jlLcs> zNG`aSgDNHnH8zWw?h!tye^aN|%>@k;h`Z_H6*py3hHO^6PE1-GSbkhG%wg;+vVo&dc)3~9&` zPtZtJyCqCdrFUIEt%Gs_?J``ycD16pKm^bZn>4xq3i>9{b`Ri6yH|K>kfC; zI5l&P)4NHPR)*R0DUcyB4!|2cir(Y1&Bsn3X8v4D(#QW8Dtv@D)CCO zadQC85Zy=Rkrhm9&csynbm>B_nwMTFah9ETdNcLU@J{haekA|9*DA2pY&A|FS*L!*O+>@Q$00FeL+2lg2NWLITxH5 z0l;yj=vQWI@q~jVn~+5MG!mV@Y`gE958tV#UcO#56hn>b69 zM;lq+P@MW=cIvIXkQmKS$*7l|}AW%6zETA2b`qD*cL z(=k4-4=t6FzQo#uMXVwF{4HvE%%tGbiOlO)Q3Y6D<5W$ z9pm>%TBUI99MC`N9S$crpOCr4sWJHP)$Zg#NXa~j?WeVo03P3}_w%##A@F|Bjo-nNxJZX%lbcyQtG8sO zWKHes>38e-!hu1$6VvY+W-z?<942r=i&i<88UGWdQHuMQjWC-rs$7xE<_-PNgC z_aIqBfG^4puRkogKc%I-rLIVF=M8jCh?C4!M|Q=_kO&3gwwjv$ay{FUDs?k7xr%jD zHreor1+#e1_;6|2wGPtz$``x}nzWQFj8V&Wm8Tu#oaqM<$BLh+Xis=Tt+bzEpC}w) z_c&qJ6u&eWHDb<>p;%F_>|`0p6kXYpw0B_3sIT@!=fWHH`M{FYdkF}*CxT|`v%pvx z#F#^4tdS0|O9M1#db%MF(5Opy;i( zL(Pc2aM4*f_Bme@o{xMrsO=)&>YKQw+)P-`FwEHR4vjU>#9~X7ElQ#sRMjR^Cd)wl zg^67Bgn9CK=WP%Ar>T4J!}DcLDe z=ehSmTp##KyQ78cmArL=IjOD6+n@jHCbOatm)#4l$t5YV?q-J86T&;>lEyK&9(XLh zr{kPuX+P8LN%rd%8&&Ia)iKX_%=j`Mr*)c)cO1`-B$XBvoT3yQCDKA>8F0KL$GpHL zPe?6dkE&T+VX=uJOjXyrq$BQ`a8H@wN1%0nw4qBI$2zBx)ID^6;Ux+? zu{?X$_1hoz9d^jkDJpT-N6+HDNo%^MQ2~yqsSBJj4@5;|1@w+BE04#@Jo4I63<~?O?ok%g%vQakTJKpMsk&oeVES1>cnaF7ZkFpqN6lx` zzD+YhR%wq2DP0fJCNC}CXK`g{AA6*}!O}%#0!Tdho4ooh&a5&{xtcFmjO4%Kj$f(1 zTk||{u|*?tAT{{<)?PmD_$JVA;dw;UF+x~|!q-EE*Oy?gFIlB*^``@ob2VL?rogtP z0M34@?2$;}n;^OAV2?o|zHg`+@Adk+&@Syd!rS zWvW$e5w{onua4sp+jHuJ&olMz#V53Z5y-FkcJDz>Wk%_J>COk5<0ya*aZLZl9LH}A zJhJ`Q-n9K+c8=0`FWE^x^xn4Fa7PDUc;v2+us(dSaoIUR4D#QQh91R!${|j{)=Zy1 zG;hqgdhSklM-VKL6HNC3&B(p1B)2Nshe7)F=-HBe=8o%OhK1MN*Gq6dBuPvqDRVJ{ z;zVNY?wSB%W0s^OMR_HL(Ws)va7eWGF*MWx<1wG7hZ}o=B62D?i|&0b14_7UG287YDr%?aYMMpeCkY1i`b+H!J9sqrvKc#Y6c8At@QiLSwj)@ifz~Z|c$lOMA@?cPqFRmZ%_>bz2X4(B=`^3;MDjsEeAO=? zSoD&+L>A|fGt7+6kF2@LqhL06sD%|~YsIe=EcWqy{e_61N_D(*CacnMvyXMjP87HI z4PT6!$fzxx{}=>jeqzkkoN+!r9e|@lZUN4pn(T28v`k=_vIhTn^i9O3qTqd)-%!QQ zYB6*6B@&b(!#X4C~59SLZuorNU_wWZA36{>O%iX)VS5NNZh49C_ppI>?)wwml}_0MLzOXT>lmo#&Ew6d?mu8~~I_^4VGBQtCAke;RQa5DL` z1PFDPsKb3CS$v;RhlQ1J@AHa1VRuuxp}NOIvrC>4$$A0Ix0VpAc0lfG%8{mR{TRQ( zbXM#1Tci3H*Wt>cVuMta^6^z`=^B@j+YhJqq9?>zZPxyg2U(wvod=uwJs{8gtpyab zXHQX<0FOGW6+dw&%c_qMUOI^+Rnb?&HB7Fee|33p4#8i>%_ev(aTm7N1f#6lV%28O zQ`tQh$VDjy8x(Lh#$rg1Kco$Bw%gULq+lc4$&HFGvLMO30QBSDvZ#*~hEHVZ`5=Kw z3y^9D512@P%d~s{x!lrHeL4!TzL`9(ITC97`Cwnn8PSdxPG@0_v{No|kfu3DbtF}K zuoP+88j4dP+Bn7hlGwU$BJy+LN6g&d3HJWMAd1P9xCXG-_P)raipYg5R{KQO$j;I9 z1y1cw#13K|&kfsRZ@qQC<>j=|OC?*v1|VrY$s=2!{}e33aQcZghqc@YsHKq^)kpkg z>B;CWNX+K=u|y#N)O>n5YuyvPl5cO6B^scmG?J zC8ix)E1PlhNaw8FpD+b|D$z`Id^4)rJe78MNiBga?Z- z0$L&MRTieSB1_E#KaN*H#Ns1}?zOA%Ybr{G+Sn3moXTVZj=L`nt?D&-MjOMz-Yq&@ z$P3h23d_F8Dcf*?txX7}p>nM*s+65t z1il8bHHsBynUK|aEXSjzY6sz1nZ%|%XeWTcGLRyRl@q4YAR)JovbdTTY&7u>@}28A zgV^Npp?}I!?3K7IXu9ml-Lw;w@9m zBYTeU+Seh8uJ-w?4e_6byq0f7>O3xm(hO}Y=fgU5^vW|>0yQ^0+?}LT55ei$i zzlU-iRbd8TRX9Ept%h%ariV=%u%F@@FA>U*XdAalcH%>#5_a&w)g`uW%3}m?vP- zc5}DkuF6ruKDwEYj+2YTSQ9=rkp19U5P@(zRm(nLod(sG9{~nw1BUoS2OFDXa{xfw zZ~UaZLFUZxfQ*9?_X?*~`d;nn-BbaefLJ`DT13KF6?T5Mnt;v5d>H}s)aAIzJcs#B z|CuXPJKww}hWBKsUfks#Kh$)ptp?5U1b@ttXFRbe_BZ&_R9XC6CA4WhWhMUE9Y2H4 z{w#CBCR<)Fd1M;mx*m?Z=L-^1kv1WKtqG(BjMiR4M^5yN4rlFM6oGUS2Wf~7Z@e*- ze84Vr`Bmi!(a1y}-m^HHMpbAiKPVEv|(7=|}D#Ihfk+-S5Hlkfch02z&$(zS3vrYz2g*ic{xBy~*gIp(eG}^gMc7 zPu2Eivnp@BH3SOgx!aJXttx*()!=2)%Bf$Gs^4cCs@)=(PJNxhH5lVY&qSZYaa?A^LhZW`B9(N?fx<^gCb(VE%3QpA*_Pohgp6vCB36iVaq zc1TI%L2Le?kuv?6Dq`H+W>AqnjyEzUBK948|DB|)U0_4DzWF#7L{agwo%y$hC>->r z4|_g_6ZC!n2=GF4RqVh6$$reQ(bG0K)i9(oC1t6kY)R@DNxicxGxejwL2sB<>l#w4 zE$QkyFI^(kZ#eE5srv*JDRIqRp2Totc8I%{jWhC$GrPWVc&gE1(8#?k!xDEQ)Tu~e zdU@aD8enALmN@%1FmWUz;4p}41)@c>Fg}1vv~q>xD}KC#sF|L&FU);^Ye|Q;1#^ps z)WmmdQI2;%?S%6i86-GD88>r|(nJackvJ#50vG6fm$1GWf*f6>oBiDKG0Kkwb17KPnS%7CKb zB7$V58cTd8x*NXg=uEX8Man_cDu;)4+P}BuCvYH6P|`x-#CMOp;%u$e z&BZNHgXz-KlbLp;j)si^~BI{!yNLWs5fK+!##G;yVWq|<>7TlosfaWN-;C@oag~V`3rZM_HN`kpF`u1p# ztNTl4`j*Lf>>3NIoiu{ZrM9&E5H~ozq-Qz@Lkbp-xdm>FbHQ2KCc8WD7kt?=R*kG# z!rQ178&ZoU(~U<;lsg@n216Ze3rB2FwqjbZ=u|J?nN%<4J9(Bl(90xevE|7ejUYm9 zg@E_xX}u2d%O1mpA2XzjRwWinvSeg)gHABeMH(2!A^g@~4l%8e0WWAkBvv60Cr>TR zQB1%EQ zUoZeUdqjh+1gFo6h~C~z#A57mf5ibmq$y_uVtA_kWv8X)CzfVEooDaY!#P?5$Y zGPKXbE<75nc%D-|w4OrP#;87oL@2^4+sxKah;a-5&z_&SUf~-z(1}bP=tM^GYtR3a z!x4zjSa^)KWG6jxfUI#{<26g$iAI;o_+B{LXY@WfWEdEl6%#8s3@b`?&Tm#aSK!~| z^%DdrXnijW`d!ajWuKApw&{L+WCPpFialo&^dZ9jC7A%BO`2ZF&YUDe;Yu|zFuv`2 z)BE*7Lkay)M7uohJ)446X``0x0%PzPTWY92`1Oq4a2D_7V0wypPnXFR)WM0IlFgg@ zqz#hv2xJEQL8eu}O;e(w4rSA?5|eZHbS6jENytJBq59?bOf>Wrl8ySZH36H(6fGR#vHM6q zn}!7!I@4$*+LFXs{x?|=q2*QtYT%Lw3+5(8uc0j8o3}TrG(zSV#>4wo6~)u|R+Yx# z?0$AspZDjv{dfv417~C17Oy%Fal{%+B6H(NX`$Bl>II-L3N3 zZc+sKZbqewU*&_Xt;9k=%4*aVYBvE1n&JZS7Uqjd%n8nOQmzh^x#vWK{;In~=QO)g zT-n3OU(1@3QfL|$g1d2xeBb@O15Rl01+hmpup2De7p%Yrd$E7(In!*R+;IJZh}v!svi z;7N~pq8KZDXXap0qd_D=Y^B)rz4S0^SF=&v6YYTAV$ad43#x!+n~-6< zK{8*vWoAdW(gGGt&URD}@g6tMoY(+Lw=vvxhfIIK9AjvNF_(W}1Rxn(mp;tJfDV<0 zbJN0t(@Xb8UeO{&T{$$uDrs7)j$}=?WsuDl+T2N5Y<4TMHGOMcocPr$%~(yvtKv(n z`U96d!D0cb9>Dx2zz$m&lAhazs%UeR^K*gb>d8CPs+?qlpfA;t{InXa)^2ryC(FU(Zc6Xbnnh`lg`K&g^JeS>}^c0MJKUCfV+~ zV(EN0Z5ztoN;hqcj!8V+VRbSltJ<~|y`U+9#wv|~H zNE!j9uXa=dec@JQSgJ6N6@Il&tzCBJv9#ldR`Lm*<)YwH4tdlAlG0Fl8Nfa(J~c%DQ2AA-}x8D=p(l#n1+hgx;N;1Aq?lq@{Lt9FKu89CjnnHD1G_@p;%Lp`+b@ttb33!E_Xt;QUD9~nRQl&xAro9-{+&6^ljK2f-d>&qy&d#0xwH z@slNv@ULKp!Cf*JHuS@#4c?F->WjPc)yiuSargAIEg>muRxzY?Hzdq@G5CS)U1*Et zE2SLh=@DI1J(guiy2Igq(?(xI9WL%g^f@{5Hmr|!Qz4`vn|LjrtO=b~I6~5EU5Fxy z;-#<)6w#w=DkpSthAu+E;OL?!?6C9Mwt*o(@68(Jhvs-eX4V z=d=>HI|`3J%H5X|gSrC8KH^IL?h5=3ID6svwHH@(wRbSG`Zsor^q4`3PCn#-(YX?< z_q8+T)51$E0xyKR{L!LN(G=+9K6$3#PDT^IAe|Igkx=!4#rqKWoXiZdh`&ocjp=Ok zemJe6*{it~>;sr(B0fSmp(S#*y5I0)OOz~Oe6Im+($S}e3tyx7Y6pA8vKCBmSEQDa zLfkm*;uMbTLpcR0)tF_v-lbK%`5>POyI2E(!)2=Rj0p;WKi=|UNt6HsQv0xR3QIK9 zsew(AFyzH!7Azxum{%VC^`cqhGdGbABGQ4cYdNBPTx+XpJ=NUEDeP^e^w^AOE1pQI zP{Us-sk!v$gj}@684E!uWjzvpoF|%v-6hwnitN1sCSg@(>RDCVgU8Ile_-xX`hL6u zzI4*Q)AVu(-ef8{#~P9STQ5t|qIMRoh&S?7Oq+cL6vxG?{NUr@k(~7^%w)P6nPbDa~4Jw}*p-|cT4p1?)!c0FoB(^DNJ+FDg+LoP6=RgB7Or673WD5MG&C!4< zerd6q$ODkBvFoy*%cpHGKSt z3uDC6Sc=xvv@kDzRD)aIO`x}BaWLycA%(w-D`Pd+uL*rL|etagQ;U&xt_9?7#}=}5HI)cU-0 z%pMA`>Xb7s)|Y)4HKSZOu;{lg=KjeIyXb0{@EM`FTDkLRH`!W%z*lQJ74P%Ka76)H zblrSIzf+dMWbO`g;=(b@{pS)zUcO&GrIFe%&?YeX4r8B2bBArB%-5ZrQ+vonr%AYy z1+u0*K{UVUmV>h5vD!F;6}a%KdMZQLs04oGkpiaC)zI( zT2U9qta5o|6Y+It1)sE8>u&0)W~l$NX@ZQ8UZfB=`($EW6?FT%{EoRhOrb9)z@3r8y?Z99FNLDE;7V=Q zotj&igu*Rh^VQn3MQKBq!T{yTwGhn1YL6k*?j?{_ek5xe8#i#GG4S-a_Re2lssG!} z`Y-d0BcOdB@!m?4y&hMN68}#0-IIlm_xO)d#}ugX{q^OZe{-@LeJyv`cY&ze4t2~! zKb{qX-j;kt{?gC(vW%}X4pm@1F?~LH{^Q8d@X$dy@5ff~p!J3zmA>H`A)y+6RB_h* zZfIO+bd=*LiymRw{asW%xxaVl33_xtdVrrqIPn zc@y8oMJvNtgcO~4i0`f)GCFkWY8EF?4duLVjHTdb6oYLnO9}Q-pe{CKQJL)hV8)JI z$mVA0Dq&7Z1TbYdSC(WbJ+IBjXngZTu&I+vHF|>Zo$757{8lL;8Zr-Exkf?3jzN5k z_d9I>{>^J?!l)< zNd$7E9FVrta}3qy3L7Ys$^fRWNuu^hs^{*eXvazd&+Q*?lTfc>2+EdP(o0P_Z05HX zVKsfFAQ{t^CRu~Dw(CuJ>tvx*p$5@flA>QRl455b&{*U?xU8`)nF2T$uu_(l8VNtq z?pBiRQIckGzk8W&SFSB=g6eG`ZC;6v9w`?eF*S}3E@N`2ropeHP)E}o?qJkyVEI;K$!)bWY zt9>4WmDVJh7U~m$|K`T#hF!v|znj^=M;69uXrFys#51XT;DbMr4H)>7UQ1e2(cuQf z4kr~Tt1tpBB2GaJ(|j~lHgW40EgMMVqR6eJoJig1SBg|2=$~4I3P0eP$q%_`sS&4~ z26=&a&tLjQbch1`cVXa-2fTl1y8}->|Nqu?uVrNTov!=VKh)g89wUPTgAzkSKZ57_ zr=B^mcldE3K04t4{;RaG53&9yovq;@aR#VHx+R1^^*kr-vEEd!uea68Z<{R%_DD6fn&T4 zu;fDj07L-(_fLSJGdkeh&c&7A(ZLj`7iwnkAcqUexU;WjUkqeg1m1-IUZTIZA(4dtr2Gr`e{BIejlCgS<33MB=1!8?a74!F%=Uo7N`F@k} ze+1C_eU4Y_$mvdjci zwEtCIphA2PBzBhng5=M#e4r%)RW5rVD|_`PvY$7BK`}w~d>%0O9sY#*LUAq=^OjMF^PY5m<7!=s5jyRfosCQAo#hL`h5vN-M}6Q z0Li}){5?wi8)GVHNkF|U9*8V5ej)nhb^TLw1KqiPK(@{P1^L&P=`ZNt?_+}&0(8Uh zfyyZFPgMV7ECt;Jdw|`|{}b$w4&x77VxR>8wUs|GQ5FBf1UlvasqX$qfk5rI4>Wfr zztH>y`=daAef**C12yJ7;LDf&3;h3X+5@dGPy@vS(RSs3CWimbTp=g \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From d4014110b30ce6767231c951a745b8eb8c5e9eab Mon Sep 17 00:00:00 2001 From: mhadam Date: Fri, 28 Dec 2018 16:22:18 -0500 Subject: [PATCH 011/128] Change sourceCompatibility and targetCompatibility to 1.8 (close #204) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 60890724..e771f838 100644 --- a/build.gradle +++ b/build.gradle @@ -31,8 +31,8 @@ apply plugin: 'propdeps-idea' group = 'com.snowplowanalytics' version = '0.8.2' -sourceCompatibility = '1.7' -targetCompatibility = '1.7' +sourceCompatibility = '1.8' +targetCompatibility = '1.8' repositories { // Use 'maven central' for resolving our dependencies From a320f2419e1a4a534c7848b171d4ee80d3e4fd7a Mon Sep 17 00:00:00 2001 From: mhadam Date: Thu, 27 Dec 2018 12:33:57 -0500 Subject: [PATCH 012/128] Prepared for release --- CHANGELOG | 14 ++++++++++++++ build.gradle | 2 +- .../snowplow/tracker/TrackerTest.java | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f5dd08e9..2c34933e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,17 @@ +Java 0.8.3 (2019-01-02) +----------------------- +Change some info statements to debug (#202) +Change `slf4j-simple` to a test runtime dependency (#188) +Close ResponseBody (#195) +Use UTF-8 encoding in events (#181) +Make tracker exit cleanly (#187) +Add simple-console sample project (#191) +Fix README.md formatting (#190) +Remove JDK7 and add OpenJDK8 in Travis build matrix (#205) +Change sourceCompatibility and targetCompatibility to 1.8 (#204) +Update Gradle to 5.0 (#203) +Add Java 11 to Travis build matrix (#207) + Java 0.8.2 (2016-02-28) ----------------------- Fixed GET requests not being properly encoded (#174) diff --git a/build.gradle b/build.gradle index e771f838..0b697f78 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ apply plugin: 'propdeps-maven' apply plugin: 'propdeps-idea' group = 'com.snowplowanalytics' -version = '0.8.2' +version = '0.8.3' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 5f5fe974..d1b7106e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -390,7 +390,7 @@ public void testTrackTimingWithSubject() { @Test public void testGetTrackerVersion() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); - assertEquals("java-0.8.2", tracker.getTrackerVersion()); + assertEquals("java-0.8.3", tracker.getTrackerVersion()); } @Test From 3540d3c65079c68fcaa14ef235115092c8ea62cf Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 9 Jan 2019 16:00:37 +0100 Subject: [PATCH 013/128] Add Bintray credentials to .travis.yml (closes #208) --- .travis.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 52bafd8e..adf02abd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,13 @@ sudo: false - language: java - jdk: - - openjdk8 - - oraclejdk8 - - openjdk11 - - oraclejdk11 +- openjdk8 +- oraclejdk8 +- openjdk11 +- oraclejdk11 +env: + global: + # BINTRAY_SNOWPLOW_MAVEN_USER + - secure: Sk7Xf0TEXyDKtZxICiDVZkDEnDkSSe3m2+j1FWhLNEVfVDGqY9j4mo84S9qNOGjblJ6LbLa91NPhGFaNa1E0WAb9Zlf7e82nELTufGmoOn006Tw/nSEy8Vpvbjh+OZ+wweGYSghWYvjYKmUtlwpwBDyHezblVmpBa9tLg/2Ajzw= + # BINTRAY_SNOWPLOW_MAVEN_API_KEY + - secure: aqeXPiW/VAZNQJiE9z2S8Z/bshyVXiepczXIDlyesKe//qNaQ3X6A5Ozh8r0KCk1TCJESW5fFzBzD1kla/aDK7clW9GFQ3U29aWgXcGcLDu4plslKK+sGt/yDhMVEpD1qjLhI9mIwj6enDCvIlEtjVnrVkqaN2pjXreemE+F2UU= From f020b79e4bd2cefc3dd63e5659a03bd0f46ee55f Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 9 Jan 2019 16:02:56 +0100 Subject: [PATCH 014/128] Add sonatype credentials to .travis.yml (closes #209) --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index adf02abd..ba8c53bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,3 +11,6 @@ env: - secure: Sk7Xf0TEXyDKtZxICiDVZkDEnDkSSe3m2+j1FWhLNEVfVDGqY9j4mo84S9qNOGjblJ6LbLa91NPhGFaNa1E0WAb9Zlf7e82nELTufGmoOn006Tw/nSEy8Vpvbjh+OZ+wweGYSghWYvjYKmUtlwpwBDyHezblVmpBa9tLg/2Ajzw= # BINTRAY_SNOWPLOW_MAVEN_API_KEY - secure: aqeXPiW/VAZNQJiE9z2S8Z/bshyVXiepczXIDlyesKe//qNaQ3X6A5Ozh8r0KCk1TCJESW5fFzBzD1kla/aDK7clW9GFQ3U29aWgXcGcLDu4plslKK+sGt/yDhMVEpD1qjLhI9mIwj6enDCvIlEtjVnrVkqaN2pjXreemE+F2UU= + - SONA_USER=snowplow + # SONA_PASS + - secure: QjjbsUJXsD/jiWXW/5vKm6obp/0SASVRxFVtVLUCee4euPTd5faCXP0gdr1IbnNW7iLbYlk+FExw2N9CzWpfjr1EWz+U405znkR6YCMMWIQ0WKgzGzEgy/19vQhPI3SPy4ymiDEh7tbDmvmMdnmtX2+btRAGWcPp2oUSlbSldCk= From 4684e57854c227daec4f2291bedba31ef65244a2 Mon Sep 17 00:00:00 2001 From: mhadam Date: Wed, 9 Jan 2019 18:18:39 -0500 Subject: [PATCH 015/128] Add deployment to build process (close #183) --- .travis.yml | 8 ++ .travis/deploy.sh | 14 ++ build.gradle | 156 ++++++++++++++++++----- gradle/wrapper/gradle-wrapper.properties | 3 +- 4 files changed, 151 insertions(+), 30 deletions(-) create mode 100755 .travis/deploy.sh diff --git a/.travis.yml b/.travis.yml index ba8c53bb..b2db1eff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,14 @@ jdk: - oraclejdk8 - openjdk11 - oraclejdk11 +script: +- "./gradlew build" +deploy: + skip_cleanup: true + provider: script + script: "./.travis/deploy.sh $TRAVIS_TAG" + on: + tags: true env: global: # BINTRAY_SNOWPLOW_MAVEN_USER diff --git a/.travis/deploy.sh b/.travis/deploy.sh new file mode 100755 index 00000000..ef8d12b4 --- /dev/null +++ b/.travis/deploy.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +tag_version=$1 + +cd $TRAVIS_BUILD_DIR +pwd + +project_version=`./gradlew -q printVersion` +if [ "${project_version}" == "${tag_version}" ]; then + ./gradlew bintrayUpload +else + echo "Tag version '${tag_version}' doesn't match version in project ('${project_version}'). Aborting!" + exit 1 +fi diff --git a/build.gradle b/build.gradle index 0b697f78..28c91fc2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -11,8 +11,6 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -wrapper.gradleVersion = '5.0' - buildscript { repositories { maven { url 'http://repo.spring.io/plugins-release' } @@ -22,6 +20,10 @@ buildscript { } } +plugins { + id "com.jfrog.bintray" version "1.8.4" +} + apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'idea' @@ -29,11 +31,16 @@ apply plugin: 'propdeps' apply plugin: 'propdeps-maven' apply plugin: 'propdeps-idea' +wrapper.gradleVersion = '5.0.0' + group = 'com.snowplowanalytics' +archivesBaseName = 'snowplow-java-tracker' version = '0.8.3' sourceCompatibility = '1.8' targetCompatibility = '1.8' +def javaVersion = JavaVersion.VERSION_1_8 + repositories { // Use 'maven central' for resolving our dependencies mavenCentral() @@ -42,6 +49,8 @@ repositories { } configure([compileJava, compileTestJava]) { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion options.encoding = 'UTF-8' } @@ -56,7 +65,6 @@ sourceSets { } dependencies { - // Apache Commons compile 'commons-codec:commons-codec:1.10' compile 'commons-net:commons-net:3.3' @@ -64,10 +72,10 @@ dependencies { // Apache HTTP optional 'org.apache.httpcomponents:httpclient:4.3.3' optional 'org.apache.httpcomponents:httpasyncclient:4.0.1' - + // Square OK HTTP optional 'com.squareup.okhttp:okhttp:2.2.0' - + // SLF4J logging API compile 'org.slf4j:slf4j-api:1.7.7' testRuntime 'org.slf4j:slf4j-simple:1.7.7' @@ -90,34 +98,15 @@ task sourceJar(type: Jar, dependsOn: 'generateSources') { from sourceSets.main.allJava } -// Publishing -publishing { - publications { - mavenJava(MavenPublication) { - artifactId 'snowplow-java-tracker' - from components.java - - artifact sourceJar { - classifier "sources" - } - } - } - repositories { - maven { - url "$buildDir/repo" // change to point to your repo, e.g. http://my.org/repo - } - } -} - task generateSources { project.ext.set("outputDir", "$projectDir/src/main/java/com/snowplowanalytics/snowplow/tracker") doFirst { println outputDir - def srcFile = new File((String)outputDir, "Version.java") + def srcFile = new File((String) outputDir, "Version.java") srcFile.parentFile.mkdirs() srcFile.write( """/* - * Copyright (c) 2014-2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2018 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -128,9 +117,7 @@ task generateSources { * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ - package com.snowplowanalytics.snowplow.tracker; - // DO NOT EDIT. AUTO-GENERATED. public class Version { static final String TRACKER = "java-$project.version"; @@ -142,3 +129,114 @@ public class Version { compileJava.dependsOn generateSources compileJava.source generateSources.outputs.files, sourceSets.main.java + + +task printVersion { + doLast { + print "$project.version" + } +} + +// custom tasks for creating source/javadoc jars +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +// add javadoc/source jar tasks as artifacts +artifacts { + archives sourcesJar, javadocJar +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + groupId group + artifactId 'snowplow-java-tracker' + version version + } + } +} + +install { + repositories.mavenInstaller { + pom.artifactId = 'snowplow-java-tracker' + pom.version = "$project.version" + pom.project { + name = 'snowplow-java-tracker' + description = 'Snowplow event tracker for Java. Add analytics to your Java desktop and server apps, servlets and games.' + url = 'https://github.com/snowplow/snowplow-java-tracker/' + inceptionYear = '2014' + + packaging = 'jar' + groupId = 'com.snowplowanalytics' + + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + scm { + connection = 'https://github.com/snowplow/snowplow-java-tracker.git' + url = 'https://github.com/snowplow/snowplow-java-tracker' + } + developers { + developer { + name = 'Snowplow Analytics Ltd' + email = 'support@snowplowanalytics.com' + organization = 'Snowplow Analytics Ltd' + organizationUrl = 'http://snowplowanalytics.com' + } + } + organization { + name = 'com.snowplowanalytics' + url = 'http://snowplowanalytics.com' + } + } + } +} + +bintray { + user = System.getenv('BINTRAY_SNOWPLOW_MAVEN_USER') + key = System.getenv('BINTRAY_SNOWPLOW_MAVEN_API_KEY') + + publish = true + + pkg { + repo = 'snowplow-maven' + name = 'snowplow-java-tracker' + + group = 'com.snowplowanalytics' + userOrg = 'snowplow' + + websiteUrl = 'https://github.com/snowplow/snowplow-java-tracker' + vcsUrl = 'https://github.com/snowplow/snowplow-java-tracker' + issueTrackerUrl = 'https://github.com/snowplow/snowplow-java-tracker/issues' + + licenses = ['Apache-2.0'] + publications = ['mavenJava'] + + version { + name = "$project.version" + gpg { + sign = true + } + mavenCentralSync { + sync = true + user = System.getenv('SONA_USER') + password = System.getenv('SONA_PASS') + close = '1' + } + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8..203de01d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Jan 09 17:33:41 EST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip From 859c3751c3ec4f0051e59260df58c4642348925b Mon Sep 17 00:00:00 2001 From: mhadam Date: Wed, 9 Jan 2019 18:23:46 -0500 Subject: [PATCH 016/128] Prepared for release --- CHANGELOG | 6 ++++++ build.gradle | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2c34933e..96575360 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Java 0.8.4 (2019-01-09) +----------------------- +Add deployment to build process (close #183) +Add sonatype credentials to .travis.yml (closes #209) +Add Bintray credentials to .travis.yml (closes #208) + Java 0.8.3 (2019-01-02) ----------------------- Change some info statements to debug (#202) diff --git a/build.gradle b/build.gradle index 28c91fc2..c9f54c35 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ wrapper.gradleVersion = '5.0.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.8.3' +version = '0.8.4' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index d1b7106e..75bda6d0 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -390,7 +390,7 @@ public void testTrackTimingWithSubject() { @Test public void testGetTrackerVersion() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); - assertEquals("java-0.8.3", tracker.getTrackerVersion()); + assertEquals("java-0.8.4", tracker.getTrackerVersion()); } @Test From 5283cc8d11a3fb185b8780a9f3530b8a723805f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Renoux?= Date: Fri, 20 Dec 2019 11:07:38 +0100 Subject: [PATCH 017/128] Add support for attaching true timestamp to events (close #178) --- .../snowplow/tracker/Tracker.java | 3 +- .../snowplow/tracker/constants/Constants.java | 2 +- .../snowplow/tracker/constants/Parameter.java | 9 ++- .../tracker/events/AbstractEvent.java | 68 +++++++++++++++++-- .../events/EcommerceTransactionItem.java | 19 +++++- .../snowplow/tracker/events/Event.java | 14 +++- .../snowplow/tracker/TrackerTest.java | 68 ++++++++++++++++--- 7 files changed, 161 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 781dacf5..be31db39 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -274,7 +274,8 @@ public void track(Event event) { this.track(Unstructured.builder() .eventData((SelfDescribingJson) event.getPayload()) .customContext(context) - .timestamp(event.getTimestamp()) + .deviceCreatedTimestamp(event.getDeviceCreatedTimestamp()) + .trueTimestamp(event.getTrueTimestamp()) .eventId(event.getEventId()) .subject(subject) .build()); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java index a1d946f2..89c90c21 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java @@ -20,7 +20,7 @@ public class Constants { public static final String PROTOCOL_VENDOR = "com.snowplowanalytics.snowplow"; public static final String PROTOCOL_VERSION = "tp2"; - public static final String SCHEMA_PAYLOAD_DATA = "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-3"; + public static final String SCHEMA_PAYLOAD_DATA = "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4"; public static final String SCHEMA_CONTEXTS = "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1"; public static final String SCHEMA_UNSTRUCT_EVENT = "iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0"; public static final String SCHEMA_SCREEN_VIEW = "iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0"; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index b026d8a3..29399d1f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -19,7 +19,14 @@ public class Parameter { public static final String DATA = "data"; public static final String EVENT = "e"; public static final String EID = "eid"; - public static final String TIMESTAMP = "dtm"; + + public static final String TRUE_TIMESTAMP = "ttm"; + + public static final String DEVICE_CREATED_TIMESTAMP = "dtm"; + + /** deprecated Indicate the specific timestamp to use. This is kept for compatibility with older versions. */ + @Deprecated + public static final String TIMESTAMP = DEVICE_CREATED_TIMESTAMP; public static final String TRACKER_VERSION = "tv"; public static final String APP_ID = "aid"; public static final String NAMESPACE = "tna"; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 6b3f9944..7b3d9889 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -39,14 +39,22 @@ public abstract class AbstractEvent implements Event { protected final List context; - protected long timestamp; + + protected long deviceCreatedTimestamp; + + /** + * The true timestamp may be null if none is set. + */ + protected Long trueTimestamp; + protected final String eventId; protected final Subject subject; public static abstract class Builder> { private List context = new LinkedList<>(); - private long timestamp = System.currentTimeMillis(); + private long deviceCreatedTimestamp = System.currentTimeMillis(); + protected Long trueTimestamp = null; private String eventId = Utils.getEventId(); private Subject subject = null; @@ -69,9 +77,34 @@ public T customContext(List context) { * @param timestamp the event timestamp as * unix epoch * @return itself + * @deprecated Use {@link #trueTimestamp} or {@link #deviceCreatedTimestamp} */ + @Deprecated public T timestamp(long timestamp) { - this.timestamp = timestamp; + return deviceCreatedTimestamp(timestamp); + } + + /** + * Adjust the device-created timestamp. This is usually not what you want, check {@link #trueTimestamp}. + * + * @param timestamp the event timestamp as + * unix epoch + * @return itself + */ + public T deviceCreatedTimestamp(long timestamp) { + this.deviceCreatedTimestamp = timestamp; + return self(); + } + + /** + * The true timestamp of that event (as determined by the user). + * + * @param timestamp the event timestamp as + * unix epoch + * @return itself + */ + public T trueTimestamp(Long timestamp) { + this.trueTimestamp = timestamp; return self(); } @@ -117,7 +150,8 @@ protected AbstractEvent(Builder builder) { Preconditions.checkArgument(!builder.eventId.isEmpty(), "eventId cannot be empty"); this.context = builder.context; - this.timestamp = builder.timestamp; + this.deviceCreatedTimestamp = builder.deviceCreatedTimestamp; + this.trueTimestamp = builder.trueTimestamp; this.eventId = builder.eventId; this.subject = builder.subject; } @@ -131,11 +165,28 @@ public List getContext() { } /** - * @return the events timestamp + * @return the event's timestamp + * @deprecated Use {@link #getTrueTimestamp()} or {@link #getDeviceCreatedTimestamp()} */ @Override public long getTimestamp() { - return this.timestamp; + return this.deviceCreatedTimestamp; + } + + /** + * @return the event's device created timestamp. + */ + @Override + public long getDeviceCreatedTimestamp() { + return deviceCreatedTimestamp; + } + + /** + * @return the event's true timestamp. + */ + @Override + public Long getTrueTimestamp() { + return trueTimestamp; } /** @@ -168,7 +219,10 @@ public Subject getSubject() { */ protected TrackerPayload putDefaultParams(TrackerPayload payload) { payload.add(Parameter.EID, getEventId()); - payload.add(Parameter.TIMESTAMP, Long.toString(getTimestamp())); + if (getTrueTimestamp()!=null) { + payload.add(Parameter.TRUE_TIMESTAMP, Long.toString(getTrueTimestamp())); + } + payload.add(Parameter.DEVICE_CREATED_TIMESTAMP, Long.toString(getDeviceCreatedTimestamp())); return payload; } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index f87dc795..eb1815aa 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -13,6 +13,7 @@ package com.snowplowanalytics.snowplow.tracker.events; // Google + import com.google.common.base.Preconditions; // This library @@ -141,9 +142,25 @@ protected EcommerceTransactionItem(Builder builder) { /** * @param timestamp the new timestamp + * @deprecated Use {@link #setTrueTimestamp(long)} or {@link #setTrueTimestamp(long)} */ + @Deprecated public void setTimestamp(long timestamp) { - this.timestamp = timestamp; + setDeviceCreatedTimestamp(timestamp); + } + + /** + * @param timestamp the new timestamp + */ + public void setTrueTimestamp(long timestamp) { + this.trueTimestamp = timestamp; + } + + /** + * @param timestamp the new timestamp + */ + public void setDeviceCreatedTimestamp(Long timestamp) { + this.deviceCreatedTimestamp = timestamp; } /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 12c45c42..ee3ba102 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -31,10 +31,22 @@ public interface Event { List getContext(); /** - * @return the events timestamp + * @return the event's timestamp + * @Deprecated Use {@link #getTrueTimestamp()} or {@link #getDeviceCreatedTimestamp()} */ + @Deprecated long getTimestamp(); + /** + * @return the event's true timestamp + */ + Long getTrueTimestamp(); + + /** + * @return the event's device created timestamp + */ + long getDeviceCreatedTimestamp(); + /** * @return the event id */ diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 75bda6d0..1293f3f3 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -78,7 +78,8 @@ public void testEcommerceEvent() { .category("category") .currency("currency") .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build(); @@ -95,7 +96,8 @@ public void testEcommerceEvent() { .currency("currency") .items(item) .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -113,6 +115,7 @@ public void testEcommerceEvent() { .put("aid", "cloudfront") .put("tr_sh", "3.0") .put("dtm", "123456") + .put("ttm", "456789") .put("tz", "Etc/UTC") .put("tr_co", "country") .put("tv", Version.TRACKER) @@ -136,6 +139,7 @@ public void testEcommerceEvent() { .put("aid", "cloudfront") .put("ti_cu", "currency") .put("dtm", "123456") + .put("ttm", "456789") .put("tz", "Etc/UTC") .put("ti_pr", "1.0") .put("ti_qu", "2") @@ -155,7 +159,8 @@ public void testUnstructuredEventWithContext() { ImmutableMap.of("foo", "bar") )) .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -173,6 +178,7 @@ public void testUnstructuredEventWithContext() { .put("tz", "Etc/UTC") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"payload\",\"data\":{\"foo\":\"bar\"}}}") .put("dtm", "123456") + .put("ttm", "456789") .put("aid", "cloudfront") .build(), result); } @@ -185,7 +191,37 @@ public void testUnstructuredEventWithoutContext() { "payload", ImmutableMap.of("foo", "baær") )) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) + .eventId(EXPECTED_EVENT_ID) + .build()); + + // Then + verify(emitter).emit(captor.capture()); + Map result = captor.getValue().getMap(); + assertEquals(ImmutableMap.builder() + .put("p", "srv") + .put("tv", Version.TRACKER) + .put("eid", EXPECTED_EVENT_ID) + .put("e", "ue") + .put("tna", "AF003") + .put("tz", "Etc/UTC") + .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"payload\",\"data\":{\"foo\":\"baær\"}}}") + .put("dtm", "123456") + .put("ttm", "456789") + .put("aid", "cloudfront") + .build(), result); + } + + @Test + public void testUnstructuredEventWithoutTrueTimestamp() { + // When + tracker.track(Unstructured.builder() + .eventData(new SelfDescribingJson( + "payload", + ImmutableMap.of("foo", "baær") + )) + .deviceCreatedTimestamp(123456) .eventId(EXPECTED_EVENT_ID) .build()); @@ -213,7 +249,8 @@ public void testTrackPageView() { .pageTitle("title") .referrer("referer") .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -222,6 +259,7 @@ public void testTrackPageView() { Map result = captor.getValue().getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") + .put("ttm", "456789") .put("tz", "Etc/UTC") .put("e", "pv") .put("page", "title") @@ -243,7 +281,8 @@ public void testTrackScreenView() { .name("name") .id("id") .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -252,6 +291,7 @@ public void testTrackScreenView() { Map result = captor.getValue().getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") + .put("ttm", "456789") .put("tz", "Etc/UTC") .put("e", "ue") .put("tv", Version.TRACKER) @@ -270,7 +310,8 @@ public void testTrackScreenViewWithTimestamp() { tracker.track(ScreenView.builder() .name("name") .id("id") - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -279,6 +320,7 @@ public void testTrackScreenViewWithTimestamp() { Map result = captor.getValue().getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") + .put("ttm", "456789") .put("tz", "Etc/UTC") .put("e", "ue") .put("tv", Version.TRACKER) @@ -297,7 +339,8 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() { .name("name") .id("id") .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -314,6 +357,7 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() { .put("tz", "Etc/UTC") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") .put("dtm", "123456") + .put("ttm", "456789") .put("aid", "cloudfront") .build(), result); } @@ -327,7 +371,8 @@ public void testTrackTiming() { .variable("variable") .timing(10) .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .build()); @@ -344,6 +389,7 @@ public void testTrackTiming() { .put("tz", "Etc/UTC") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") .put("dtm", "123456") + .put("ttm", "456789") .put("aid", "cloudfront") .build(), result); } @@ -362,7 +408,8 @@ public void testTrackTimingWithSubject() { .variable("variable") .timing(10) .customContext(contexts) - .timestamp(123456) + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) .eventId(EXPECTED_EVENT_ID) .subject(s1) .build()); @@ -381,6 +428,7 @@ public void testTrackTimingWithSubject() { .put("tna", "AF003") .put("tz", "Etc/UTC") .put("dtm", "123456") + .put("ttm", "456789") .put("aid", "cloudfront") .build(), result); } From d666cce8c34605ee15bf70ce839295aa45df870e Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Thu, 19 Dec 2019 12:38:46 +0000 Subject: [PATCH 018/128] Update travis builds to use trusty (close #215) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b2db1eff..bc971905 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ sudo: false +dist: trusty language: java jdk: - openjdk8 From 5dd442eed34183449bb1334ef6b36c6a6cfa201c Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Fri, 20 Dec 2019 10:30:33 +0000 Subject: [PATCH 019/128] Fix Javadoc generation warnings (close #219) --- .../java/com/snowplowanalytics/snowplow/tracker/Utils.java | 1 + .../snowplow/tracker/events/AbstractEvent.java | 2 +- .../snowplow/tracker/events/EcommerceTransactionItem.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Event.java | 2 +- .../snowplow/tracker/http/HttpClientAdapter.java | 2 ++ .../snowplow/tracker/payload/SelfDescribingJson.java | 4 ++++ 6 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index 49a76286..a59c7ac0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -107,6 +107,7 @@ public static String getTimezone() { * Encodes a string into Base64. * * @param string the string too encode + * @param charset the charset used when base64 encoding string * @return a Base64 encoded string */ public static String base64Encode(String string, Charset charset) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 7b3d9889..8337541f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -77,7 +77,7 @@ public T customContext(List context) { * @param timestamp the event timestamp as * unix epoch * @return itself - * @deprecated Use {@link #trueTimestamp} or {@link #deviceCreatedTimestamp} + * Use {@link #trueTimestamp} or {@link #deviceCreatedTimestamp} */ @Deprecated public T timestamp(long timestamp) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index eb1815aa..33c9b789 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -142,7 +142,7 @@ protected EcommerceTransactionItem(Builder builder) { /** * @param timestamp the new timestamp - * @deprecated Use {@link #setTrueTimestamp(long)} or {@link #setTrueTimestamp(long)} + * Use {@link #setTrueTimestamp(long)} or {@link #setTrueTimestamp(long)} */ @Deprecated public void setTimestamp(long timestamp) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index ee3ba102..2a65d54a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -32,7 +32,7 @@ public interface Event { /** * @return the event's timestamp - * @Deprecated Use {@link #getTrueTimestamp()} or {@link #getDeviceCreatedTimestamp()} + * Use {@link #getTrueTimestamp()} or {@link #getDeviceCreatedTimestamp()} */ @Deprecated long getTimestamp(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java index 53227bcd..f858b62c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java @@ -26,6 +26,7 @@ public interface HttpClientAdapter { * single SelfDescribingJson payload * * @param payload the final event payload + * @return status code */ int post(SelfDescribingJson payload); @@ -34,6 +35,7 @@ public interface HttpClientAdapter { * GET request * * @param payload the event payload + * @return status code */ int get(TrackerPayload payload); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index b224294e..54a5c5c0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -90,6 +90,7 @@ public SelfDescribingJson(String schema, Object data) { * Sets the Schema for the SelfDescribingJson * * @param schema a valid schema string + * @return this SelfDescribingJson */ public SelfDescribingJson setSchema(String schema) { Preconditions.checkNotNull(schema, "schema cannot be null"); @@ -103,6 +104,7 @@ public SelfDescribingJson setSchema(String schema) { * - Accepts a TrackerPayload object * * @param data the data to be added to the SelfDescribingJson + * @return this SelfDescribingJson */ public SelfDescribingJson setData(TrackerPayload data) { if (data == null) { @@ -116,6 +118,7 @@ public SelfDescribingJson setData(TrackerPayload data) { * Adds data to the SelfDescribingJson * * @param data the data to be added to the SelfDescribingJson + * @return this SelfDescribingJson */ public SelfDescribingJson setData(Object data) { if (data == null) { @@ -130,6 +133,7 @@ public SelfDescribingJson setData(Object data) { * without copying over the Schema. * * @param data the payload to add to the SelfDescribingJson + * @return this SelfDescribingJson */ public SelfDescribingJson setData(SelfDescribingJson data) { if (payload == null) { From f76e805aecd27868c59277a73eb5736fcd9aa5e4 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Thu, 19 Dec 2019 13:58:34 +0000 Subject: [PATCH 020/128] Fix Peru version so vagrant up succeeds (close #216) --- vagrant/up.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vagrant/up.bash b/vagrant/up.bash index 7450ae89..8613cd82 100755 --- a/vagrant/up.bash +++ b/vagrant/up.bash @@ -13,7 +13,7 @@ apt-get install -y language-pack-en git unzip libyaml-dev python3-pip python-yam echo "===============" echo "INSTALLING PERU" echo "---------------" -sudo pip3 install peru +sudo pip3 install peru==1.1.4 echo "=======================================" echo "CLONING ANSIBLE AND PLAYBOOKS WITH PERU" From fef8e7289d7b62b4489ebf082452a292853c0728 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Fri, 20 Dec 2019 11:34:28 +0000 Subject: [PATCH 021/128] Update all non-static Loggers to static (close #213) --- .../snowplow/tracker/http/ApacheHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/OkHttpClientAdapter.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJson.java | 2 +- .../snowplow/tracker/payload/TrackerPayload.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 2cd7b279..97e9d5ab 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -40,7 +40,7 @@ */ public class ApacheHttpClientAdapter extends AbstractHttpClientAdapter { - private final Logger LOGGER = LoggerFactory.getLogger(ApacheHttpClientAdapter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ApacheHttpClientAdapter.class); private CloseableHttpClient httpClient; public static abstract class Builder> extends AbstractHttpClientAdapter.Builder { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 11a157eb..fb5b18b1 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -39,7 +39,7 @@ */ public class OkHttpClientAdapter extends AbstractHttpClientAdapter { - private final Logger LOGGER = LoggerFactory.getLogger(OkHttpClientAdapter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(OkHttpClientAdapter.class); private final MediaType JSON = MediaType.parse(Constants.POST_CONTENT_TYPE); private OkHttpClient httpClient; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index 54a5c5c0..18cc98d8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -34,7 +34,7 @@ */ public class SelfDescribingJson implements Payload { - private final Logger LOGGER = LoggerFactory.getLogger(SelfDescribingJson.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SelfDescribingJson.class); private final LinkedHashMap payload = new LinkedHashMap<>(); /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index a1715ea4..c7f764b8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -30,7 +30,7 @@ */ public class TrackerPayload implements Payload { - private final Logger LOGGER = LoggerFactory.getLogger(TrackerPayload.class); + private static final Logger LOGGER = LoggerFactory.getLogger(TrackerPayload.class); private final LinkedHashMap payload = new LinkedHashMap<>(); /** From bbbc0d26a7339f3940f320347f0678c22f4a2ea2 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 23 Dec 2019 17:00:10 +0000 Subject: [PATCH 022/128] Bump OkHttp to OkHttp3 version 4 (close #175) --- .gitignore | 2 + build.gradle | 6 +- examples/simple-console/build.gradle | 11 +-- .../main/java/com/snowplowanalytics/Main.java | 12 ++-- .../http/AbstractHttpClientAdapter.java | 2 +- .../tracker/http/OkHttpClientAdapter.java | 70 +++++++------------ .../tracker/http/HttpClientAdapterTest.java | 21 +++--- 7 files changed, 57 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 8be46618..6887f746 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ Version.java # Vagrant .vagrant +#macOS +.DS_Store diff --git a/build.gradle b/build.gradle index c9f54c35..6c92f4cc 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,7 @@ dependencies { optional 'org.apache.httpcomponents:httpasyncclient:4.0.1' // Square OK HTTP - optional 'com.squareup.okhttp:okhttp:2.2.0' + optional 'com.squareup.okhttp3:okhttp:4.2.2' // SLF4J logging API compile 'org.slf4j:slf4j-api:1.7.7' @@ -90,8 +90,8 @@ dependencies { testCompile 'junit:junit:4.11' testCompile 'com.github.tomakehurst:wiremock:1.53' testCompile 'org.skyscreamer:jsonassert:1.2.3' - testCompile 'org.mockito:mockito-core:1.9.5' - testCompile 'com.squareup.okhttp:mockwebserver:2.1.0' + testCompile 'org.mockito:mockito-core:3.2.4' + testCompile 'com.squareup.okhttp3:mockwebserver:4.2.1' } task sourceJar(type: Jar, dependsOn: 'generateSources') { diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 3682679f..a865c365 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -4,14 +4,17 @@ version = '0.0.1' repositories { mavenCentral() - maven { - url "http://maven.snplow.com/releases" - } + flatDir { + dirs '../../build/libs' + } } dependencies { compile 'com.google.code.gson:gson:2.8+' - compile 'com.snowplowanalytics:snowplow-java-tracker:0.8.0' + compile 'com.snowplowanalytics:snowplow-java-tracker:0.8.4' + optional 'com.squareup.okhttp3:okhttp:4.2.2' + compile 'org.slf4j:slf4j-api:1.7.7' + compile 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' testCompile 'junit:junit:4.12' } diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 2ab1ab48..0c9c1ff8 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -22,7 +22,7 @@ import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import com.squareup.okhttp.OkHttpClient; +import okhttp3.OkHttpClient; import java.util.List; import java.util.concurrent.TimeUnit; @@ -40,11 +40,11 @@ public static String getUrlFromArgs(String[] args) { public static HttpClientAdapter getClient(String url) { // use okhttp to send events - OkHttpClient client = new OkHttpClient(); - - client.setConnectTimeout(5, TimeUnit.SECONDS); - client.setReadTimeout(5, TimeUnit.SECONDS); - client.setWriteTimeout(5, TimeUnit.SECONDS); + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .build(); return OkHttpClientAdapter.builder() .url(url) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index e69c104b..0468aabd 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -43,7 +43,7 @@ public static abstract class Builder> { * @return itself */ public T url(String url) { - this.url = url; + this.url = url.replaceFirst("/*$", ""); return self(); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index fb5b18b1..187121f6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -19,11 +19,11 @@ import com.google.common.base.Preconditions; // SquareUp -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.MediaType; -import com.squareup.okhttp.Response; -import com.squareup.okhttp.RequestBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.MediaType; +import okhttp3.Response; +import okhttp3.RequestBody; // Slf4j import org.apache.http.HttpHeaders; @@ -40,7 +40,7 @@ public class OkHttpClientAdapter extends AbstractHttpClientAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(OkHttpClientAdapter.class); - private final MediaType JSON = MediaType.parse(Constants.POST_CONTENT_TYPE); + private final MediaType JSON = MediaType.get(Constants.POST_CONTENT_TYPE); private OkHttpClient httpClient; public static abstract class Builder> extends AbstractHttpClientAdapter.Builder { @@ -99,19 +99,18 @@ public Object getHttpClient() { * @return the HttpResponse code for the Request or -1 if exception is caught */ public int doGet(String url) { - - Response response = null; int returnValue = -1; Request request = new Request.Builder().url(url).build(); - try { - response = httpClient.newCall(request).execute(); - returnValue = response.code(); - } catch (Exception e) { + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + LOGGER.error("OkHttpClient GET Request failed: {}", response); + } else { + returnValue = response.code(); + } + } catch (IOException e) { LOGGER.error("OkHttpClient GET Request failed: {}", e.getMessage()); - } finally { - closeResponseBody(response); } return returnValue; @@ -127,39 +126,24 @@ public int doGet(String url) { * @return the HttpResponse code for the Request or -1 if exception is caught */ public int doPost(String url, String payload) { - Response response = null; int returnValue = -1; - try { - RequestBody body = RequestBody.create(JSON, payload); - Request request = new Request.Builder() - .url(url) - .addHeader(HttpHeaders.CONTENT_TYPE, Constants.POST_CONTENT_TYPE) - .post(body) - .build(); - response = httpClient.newCall(request).execute(); - returnValue = response.code(); - } catch (Exception e) { + RequestBody body = RequestBody.create(JSON, payload); + Request request = new Request.Builder() + .url(url) + .addHeader(HttpHeaders.CONTENT_TYPE, Constants.POST_CONTENT_TYPE) + .post(body) + .build(); + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + LOGGER.error("OkHttpClient POST Request failed: {}", response); + } else { + returnValue = response.code(); + } + } catch (IOException e) { LOGGER.error("OkHttpClient POST Request failed: {}", e.getMessage()); - } finally { - closeResponseBody(response); } - + return returnValue; } - - - /** - * Closes response body as required by OkHttpClient documentation - * - * @param response OkHttpClient response - */ - private void closeResponseBody(Response response) { - if (response != null && response.body() != null) - try { - response.body().close(); - } catch (IOException e) { - LOGGER.error("OkHttpClient response body closing failed: {}", e.getMessage()); - } - } } \ No newline at end of file diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 61dfceb7..de437443 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -22,10 +22,10 @@ import com.google.common.collect.ImmutableMap; // SquareUp -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.mockwebserver.MockResponse; -import com.squareup.okhttp.mockwebserver.MockWebServer; -import com.squareup.okhttp.mockwebserver.RecordedRequest; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; // Apache import org.apache.http.impl.client.HttpClients; @@ -70,10 +70,11 @@ public HttpClientAdapter provide(String url) { {new HttpClientAdapterProvider() { @Override public HttpClientAdapter provide(String url) { - OkHttpClient httpClient = new OkHttpClient(); - httpClient.setConnectTimeout(1, TimeUnit.SECONDS); - httpClient.setReadTimeout(1, TimeUnit.SECONDS); - httpClient.setWriteTimeout(1, TimeUnit.SECONDS); + OkHttpClient httpClient = new OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(1, TimeUnit.SECONDS) + .writeTimeout(1, TimeUnit.SECONDS) + .build(); return OkHttpClientAdapter.builder() .url(url) .httpClient(httpClient) @@ -86,8 +87,8 @@ public HttpClientAdapter provide(String url) { public HttpClientAdapterTest(HttpClientAdapterProvider httpClientAdapterProvider) throws IOException { mockWebServer = new MockWebServer(); - mockWebServer.play(); - adapter = httpClientAdapterProvider.provide(mockWebServer.getUrl("").toString()); + mockWebServer.start(); + adapter = httpClientAdapterProvider.provide(mockWebServer.url("/").toString()); } @Test From 08b2deb5d7afe990fe4b579a6deffcab00572853 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 23 Dec 2019 17:15:09 +0000 Subject: [PATCH 023/128] Fix events sent by example simple-console (close #221) --- examples/simple-console/build.gradle | 4 +++- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../src/main/java/com/snowplowanalytics/Main.java | 15 ++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index a865c365..784bd6df 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -12,9 +12,11 @@ repositories { dependencies { compile 'com.google.code.gson:gson:2.8+' compile 'com.snowplowanalytics:snowplow-java-tracker:0.8.4' - optional 'com.squareup.okhttp3:okhttp:4.2.2' + compile 'com.squareup.okhttp3:okhttp:4.2.2' compile 'org.slf4j:slf4j-api:1.7.7' + compile 'org.slf4j:slf4j-simple:1.7.7' compile 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' + compile 'com.google.guava:guava:18.0' testCompile 'junit:junit:4.12' } diff --git a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties index 958d54cd..8b6359a4 100644 --- a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties +++ b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-bin.zip diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 0c9c1ff8..f84cf47d 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -29,7 +29,7 @@ public class Main { - private static final int PAGEVIEW_COUNT = 20; + private static final int PAGEVIEW_COUNT = 10; public static String getUrlFromArgs(String[] args) { if (args == null || args.length < 1) { @@ -91,13 +91,14 @@ public void onFailure(int successCount, List failedEvents) { .platform(DevicePlatform.ServerSideApp) .build(); - // This is a sample page view event, many other event types (such as self-describing events) are available - PageView pageViewEvent = PageView.builder() - .pageTitle("Hello world") - .pageUrl("http://helloworld.com") - .build(); - for (int i = 0; i < PAGEVIEW_COUNT; i++) { + // This is a sample page view event, many other event types (such as self-describing events) are available + PageView pageViewEvent = PageView.builder() + .pageTitle("Hello world " + i) + .pageUrl("https://www.snowplowanalytics.com") + .referrer("https://www.google.com") + .build(); + tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow } From bc0bab761678a9b3b7aec8c7bd4d6d49bf82f091 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 23 Dec 2019 17:24:47 +0000 Subject: [PATCH 024/128] Alter logging for invalid keys only when adding to TrackerPayload (close #186) --- .../snowplow/tracker/payload/TrackerPayload.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index c7f764b8..08342b37 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -43,8 +43,12 @@ public class TrackerPayload implements Payload { */ @Override public void add(String key, String value) { - if (key == null || key.isEmpty() || value == null || value.isEmpty()) { - LOGGER.error("Invalid kv pair detected: {}->{}", key, value); + if (key == null || key.isEmpty()) { + LOGGER.error("Invalid key detected: {}", key); + return; + } + if (value == null || value.isEmpty()) { + LOGGER.info("null or empty value detected: {}", value); return; } LOGGER.debug("Adding new kv pair: {}->{}", key, value); From d2f9610790c73015967510c65eab6e92bc7204f4 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Tue, 24 Dec 2019 09:34:19 +0000 Subject: [PATCH 025/128] Add STM to outbound events (close #169) --- .../snowplow/tracker/constants/Parameter.java | 1 + .../tracker/emitter/AbstractEmitter.java | 36 ++++++++---------- .../tracker/emitter/BatchEmitter.java | 37 +++++++++---------- .../tracker/emitter/SimpleEmitter.java | 13 ++++--- .../tracker/emitter/BatchEmitterTest.java | 24 ++++++++++++ 5 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index 29399d1f..16ce945d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -23,6 +23,7 @@ public class Parameter { public static final String TRUE_TIMESTAMP = "ttm"; public static final String DEVICE_CREATED_TIMESTAMP = "dtm"; + public static final String DEVICE_SENT_TIMESTAMP = "stm"; /** deprecated Indicate the specific timestamp to use. This is kept for compatibility with older versions. */ @Deprecated diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index 46ccfc2d..e3fd3863 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -50,19 +50,19 @@ public static abstract class Builder> { * @param httpClientAdapter the adapter to use * @return itself */ - public T httpClientAdapter(HttpClientAdapter httpClientAdapter) { + public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { this.httpClientAdapter = httpClientAdapter; return self(); } /** - * An optional Request Callback for adding the ability to - * handle failure cases for sending. + * An optional Request Callback for adding the ability to handle failure cases + * for sending. * * @param requestCallback the emitter request callback * @return itself */ - public T requestCallback(RequestCallback requestCallback) { + public T requestCallback(final RequestCallback requestCallback) { this.requestCallback = requestCallback; return self(); } @@ -73,7 +73,7 @@ public T requestCallback(RequestCallback requestCallback) { * @param threadCount the size of the thread pool * @return itself */ - public T threadCount(int threadCount) { + public T threadCount(final int threadCount) { this.threadCount = threadCount; return self(); } @@ -90,7 +90,7 @@ public static Builder builder() { return new Builder2(); } - protected AbstractEmitter(Builder builder) { + protected AbstractEmitter(final Builder builder) { // Precondition checks Preconditions.checkNotNull(builder.httpClientAdapter); @@ -102,8 +102,8 @@ protected AbstractEmitter(Builder builder) { } /** - * Adds a payload to the buffer and checks whether - * we have reached the buffer limit yet. + * Adds a payload to the buffer and checks whether we have reached the buffer + * limit yet. * * @param payload an event payload */ @@ -111,29 +111,25 @@ protected AbstractEmitter(Builder builder) { public abstract void emit(TrackerPayload payload); /** - * Customize the emitter buffer size to any valid integer - * greater than zero. - * - Will only effect the BatchEmitter + * Customize the emitter buffer size to any valid integer greater than zero. - + * Will only effect the BatchEmitter * - * @param bufferSize number of events to collect before - * sending + * @param bufferSize number of events to collect before sending */ @Override - public void setBufferSize(int bufferSize) { + public void setBufferSize(final int bufferSize) { Preconditions.checkArgument(bufferSize > 0, "bufferSize must be greater than 0"); this.bufferSize = bufferSize; } /** - * When the buffer limit is reached sending of the buffer is - * initiated. + * When the buffer limit is reached sending of the buffer is initiated. */ @Override public abstract void flushBuffer(); /** - * Gets the Emitter Buffer Size - * - Will always be 1 for SimpleEmitter + * Gets the Emitter Buffer Size - Will always be 1 for SimpleEmitter * * @return the buffer size */ @@ -157,7 +153,7 @@ public List getBuffer() { * * @param runnable the runnable to be queued */ - protected void execute(Runnable runnable) { + protected void execute(final Runnable runnable) { this.executor.execute(runnable); } @@ -167,7 +163,7 @@ protected void execute(Runnable runnable) { * @param code the response code * @return whether it is in the success range */ - protected boolean isSuccessfulSend(int code) { + protected boolean isSuccessfulSend(final int code) { return code >= 200 && code < 300; } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index c9d00f10..fcfda15f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -28,6 +28,7 @@ // This library import com.snowplowanalytics.snowplow.tracker.constants.Constants; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -39,7 +40,7 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); - private long closeTimeout = 5; + private final long closeTimeout = 5; public static abstract class Builder> extends AbstractEmitter.Builder { @@ -49,7 +50,7 @@ public static abstract class Builder> extends AbstractEmitt * @param bufferSize The count of events to buffer before sending * @return itself */ - public T bufferSize(int bufferSize) { + public T bufferSize(final int bufferSize) { this.bufferSize = bufferSize; return self(); } @@ -70,7 +71,7 @@ public static Builder builder() { return new Builder2(); } - protected BatchEmitter(Builder builder) { + protected BatchEmitter(final Builder builder) { super(builder); // Precondition checks @@ -80,13 +81,13 @@ protected BatchEmitter(Builder builder) { } /** - * Adds a payload to the buffer and checks whether - * we have reached the buffer limit yet. + * Adds a payload to the buffer and checks whether we have reached the buffer + * limit yet. * * @param payload an event payload */ @Override - public synchronized void emit(TrackerPayload payload) { + public synchronized void emit(final TrackerPayload payload) { buffer.add(payload); if (buffer.size() >= bufferSize) { flushBuffer(); @@ -94,8 +95,7 @@ public synchronized void emit(TrackerPayload payload) { } /** - * When the buffer limit is reached sending of the buffer is - * initiated. + * When the buffer limit is reached sending of the buffer is initiated. */ public void flushBuffer() { execute(getRequestRunnable(buffer)); @@ -116,8 +116,8 @@ public void run() { return; } - SelfDescribingJson post = getFinalPost(buffer); - int code = httpClientAdapter.post(post); + final SelfDescribingJson post = getFinalPost(buffer); + final int code = httpClientAdapter.post(post); // Process results int success = 0; @@ -143,21 +143,18 @@ public void run() { } /** - * Constructs the SelfDescribingJson to be sent - * to the endpoint + * Constructs the SelfDescribingJson to be sent to the endpoint * * @return the constructed POST payload */ - private SelfDescribingJson getFinalPost(List buffer) { - List toSendPayloads = new ArrayList<>(); - for (TrackerPayload payload : buffer) { + private SelfDescribingJson getFinalPost(final List buffer) { + final List toSendPayloads = new ArrayList<>(); + for (final TrackerPayload payload : buffer) { + payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); toSendPayloads.add(payload.getMap()); } - return new SelfDescribingJson( - Constants.SCHEMA_PAYLOAD_DATA, - toSendPayloads - ); + return new SelfDescribingJson(Constants.SCHEMA_PAYLOAD_DATA, toSendPayloads); } /** @@ -174,7 +171,7 @@ public void close() { if (!executor.awaitTermination(closeTimeout, TimeUnit.SECONDS)) LOGGER.warn("Executor did not terminate"); } - } catch (InterruptedException ie) { + } catch (final InterruptedException ie) { executor.shutdownNow(); Thread.currentThread().interrupt(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 9493f678..823801bd 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -22,6 +22,7 @@ // This library import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; /** * An emitter which sends events as soon as they are received via @@ -48,7 +49,7 @@ public static Builder builder() { return new Builder2(); } - protected SimpleEmitter(Builder builder) { + protected SimpleEmitter(final Builder builder) { super(builder); } @@ -58,13 +59,12 @@ protected SimpleEmitter(Builder builder) { * @param payload an event payload */ @Override - public void emit(TrackerPayload payload) { + public void emit(final TrackerPayload payload) { execute(getRequestRunnable(payload)); } /** - * When the buffer limit is reached sending of the buffer is - * initiated. + * When the buffer limit is reached sending of the buffer is initiated. */ public void flushBuffer() { // Do nothing! @@ -80,7 +80,8 @@ private Runnable getRequestRunnable(final TrackerPayload payload) { return new Runnable() { @Override public void run() { - int code = httpClientAdapter.get(payload); + payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); + final int code = httpClientAdapter.get(payload); // Process results int success = 0; @@ -96,7 +97,7 @@ public void run() { // Send the callback if available if (requestCallback != null) { if (failure != 0) { - List buffer = new ArrayList<>(); + final List buffer = new ArrayList<>(); buffer.add(payload); requestCallback.onFailure(success, buffer); } else { diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 5b4a85f3..53c4e6cf 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -35,6 +35,7 @@ // This library import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; public class BatchEmitterTest { @@ -108,6 +109,29 @@ public void setBufferSize_WithNegativeValue_ThrowInvalidArgumentException() thro emitter.setBufferSize(-1); } + @Test + @SuppressWarnings("unchecked") + public void getFinalPost_shouldAddSTMParameter() throws Exception { + // Given + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); + List payloads = createPayloads(10); + + // When + for (TrackerPayload payload : payloads) { + emitter.emit(payload); + } + + Thread.sleep(500); + + // Then + verify(httpClientAdapter).post(argumentCaptor.capture()); + + ArrayList> dataList = (ArrayList>) argumentCaptor.getValue().getMap().get(Parameter.DATA); + for (Map payloadMap : dataList) { + Assert.assertTrue(payloadMap.containsKey(Parameter.DEVICE_SENT_TIMESTAMP)); + } + } + private List createPayloads(int nbPayload) { final List payloads = Lists.newArrayList(); for (int i = 0; i < nbPayload; i++) { From 1f9e892a1df5d2b6ad5d6ad8aafb2b5496a6ad00 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Tue, 24 Dec 2019 09:47:13 +0000 Subject: [PATCH 026/128] Prepare for release --- .travis.yml | 1 + CHANGELOG | 11 +++++++++++ build.gradle | 4 ++-- examples/simple-console/build.gradle | 2 +- .../snowplow/tracker/TrackerTest.java | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index bc971905..5a3f50fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ deploy: provider: script script: "./.travis/deploy.sh $TRAVIS_TAG" on: + condition: '"${TRAVIS_JDK_VERSION}" == "openjdk8"' tags: true env: global: diff --git a/CHANGELOG b/CHANGELOG index 96575360..103901a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,14 @@ +Java 0.9.0 (2019-12-24) +----------------------- +Bump OkHttp to OkHttp3 version 4 (close #175) +Add STM to outbound events (close #169) +Add support for attaching true timestamp to events (close #178) +Update all non-static Loggers to static (close #213) +Fix events sent by example simple-console (close #221) +Alter logging for invalid keys only when adding to TrackerPayload (close #186) +Fix Peru version so vagrant up succeeds (close #216) +Fix Javadoc generation warnings (close #219) + Java 0.8.4 (2019-01-09) ----------------------- Add deployment to build process (close #183) diff --git a/build.gradle b/build.gradle index 6c92f4cc..1c0761ca 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ wrapper.gradleVersion = '5.0.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.8.4' +version = '0.9.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' @@ -106,7 +106,7 @@ task generateSources { srcFile.parentFile.mkdirs() srcFile.write( """/* - * Copyright (c) 2014-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 784bd6df..31fbde6e 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -11,7 +11,7 @@ repositories { dependencies { compile 'com.google.code.gson:gson:2.8+' - compile 'com.snowplowanalytics:snowplow-java-tracker:0.8.4' + compile 'com.snowplowanalytics:snowplow-java-tracker:0.9.0' compile 'com.squareup.okhttp3:okhttp:4.2.2' compile 'org.slf4j:slf4j-api:1.7.7' compile 'org.slf4j:slf4j-simple:1.7.7' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 1293f3f3..bbc93ced 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -438,7 +438,7 @@ public void testTrackTimingWithSubject() { @Test public void testGetTrackerVersion() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); - assertEquals("java-0.8.4", tracker.getTrackerVersion()); + assertEquals("java-0.9.0", tracker.getTrackerVersion()); } @Test From a42bd379d2fedf0405f0d98f713692d7234eb1e7 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Fri, 27 Mar 2020 16:17:18 +0000 Subject: [PATCH 027/128] Switch build.gradle to use https://repo.spring.io/plugins-release (close #223) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1c0761ca..304060e4 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { repositories { - maven { url 'http://repo.spring.io/plugins-release' } + maven { url 'https://repo.spring.io/plugins-release' } } dependencies { classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' From 72c9d11176e94fc6ee91467127b0b366d6a3688d Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Sun, 10 May 2020 12:06:50 +0100 Subject: [PATCH 028/128] Remove use of deprecated OkHttp methods (close #228) --- .../snowplow/tracker/http/OkHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/HttpClientAdapterTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 187121f6..34f17a82 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -128,7 +128,7 @@ public int doGet(String url) { public int doPost(String url, String payload) { int returnValue = -1; - RequestBody body = RequestBody.create(JSON, payload); + RequestBody body = RequestBody.create(payload, JSON); Request request = new Request.Builder() .url(url) .addHeader(HttpHeaders.CONTENT_TYPE, Constants.POST_CONTENT_TYPE) diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index de437443..80f1f520 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -121,7 +121,7 @@ public void post_withSuccessfulStatusCode_isOk() throws InterruptedException { assertEquals(1, mockWebServer.getRequestCount()); RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertEquals("/com.snowplowanalytics.snowplow/tp2", recordedRequest.getPath()); - assertEquals("{\"schema\":\"schema\",\"data\":{\"foo\":\"bar\"}}", recordedRequest.getUtf8Body()); + assertEquals("{\"schema\":\"schema\",\"data\":{\"foo\":\"bar\"}}", recordedRequest.getBody().readUtf8()); assertEquals("POST", recordedRequest.getMethod()); assertEquals("application/json; charset=utf-8", recordedRequest.getHeader("Content-Type")); } From 9340c99db494c9eb94fd35f8915d5fd2367df92f Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Sun, 7 Jun 2020 20:53:38 +0100 Subject: [PATCH 029/128] Add POM information to Maven Publishing section in build.gradle (close #234) --- build.gradle | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/build.gradle b/build.gradle index 304060e4..fe8491b2 100644 --- a/build.gradle +++ b/build.gradle @@ -162,6 +162,39 @@ publishing { groupId group artifactId 'snowplow-java-tracker' version version + pom { + name = 'snowplow-java-tracker' + description = 'Snowplow event tracker for Java. Add analytics to your Java desktop and server apps, servlets and games.' + url = 'https://github.com/snowplow/snowplow-java-tracker/' + inceptionYear = '2014' + + packaging = 'jar' + groupId = 'com.snowplowanalytics' + + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + scm { + connection = 'https://github.com/snowplow/snowplow-java-tracker.git' + url = 'https://github.com/snowplow/snowplow-java-tracker' + } + developers { + developer { + name = 'Snowplow Analytics Ltd' + email = 'support@snowplowanalytics.com' + organization = 'Snowplow Analytics Ltd' + organizationUrl = 'http://snowplowanalytics.com' + } + } + organization { + name = 'com.snowplowanalytics' + url = 'http://snowplowanalytics.com' + } + } } } } From 0f790f5cec8f439ec167f6b0adb6d2ed0beef577 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 11:56:56 +0100 Subject: [PATCH 030/128] Support for creating TrackerPayload asynchronously (close #222) Co-authored-by: bbplanon --- build.gradle | 2 +- .../main/java/com/snowplowanalytics/Main.java | 116 +++++++++--- .../snowplow/tracker/Subject.java | 8 + .../snowplow/tracker/Tracker.java | 169 ++---------------- .../snowplow/tracker/Utils.java | 12 +- .../tracker/emitter/AbstractEmitter.java | 38 ++-- .../tracker/emitter/BatchEmitter.java | 158 ++++++++++++---- .../snowplow/tracker/emitter/Emitter.java | 17 +- .../tracker/emitter/RequestCallback.java | 10 +- .../tracker/emitter/SimpleEmitter.java | 84 ++++++--- .../snowplow/tracker/events/Event.java | 2 - .../snowplow/tracker/events/ScreenView.java | 2 - .../snowplow/tracker/events/Unstructured.java | 6 +- .../http/AbstractHttpClientAdapter.java | 6 - .../tracker/http/ApacheHttpClientAdapter.java | 8 - .../snowplow/tracker/payload/Payload.java | 5 +- .../tracker/payload/SelfDescribingJson.java | 8 +- .../tracker/payload/TrackerEvent.java | 164 +++++++++++++++++ .../tracker/payload/TrackerParameters.java | 58 ++++++ .../tracker/payload/TrackerPayload.java | 44 +++-- .../snowplow/tracker/TrackerTest.java | 103 ++++++++--- .../tracker/emitter/BatchEmitterTest.java | 131 ++++++++++---- .../tracker/http/HttpClientAdapterTest.java | 6 - 23 files changed, 753 insertions(+), 404 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java diff --git a/build.gradle b/build.gradle index fe8491b2..d94f427a 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,6 @@ sourceSets { dependencies { // Apache Commons - compile 'commons-codec:commons-codec:1.10' compile 'commons-net:commons-net:3.3' // Apache HTTP @@ -88,6 +87,7 @@ dependencies { // Testing libraries testCompile 'junit:junit:4.11' + testCompile 'org.hamcrest:hamcrest:2.2' testCompile 'com.github.tomakehurst:wiremock:1.53' testCompile 'org.skyscreamer:jsonassert:1.2.3' testCompile 'org.mockito:mockito-core:3.2.4' diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index f84cf47d..a345bba9 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -18,18 +18,23 @@ import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.emitter.RequestCallback; -import com.snowplowanalytics.snowplow.tracker.events.PageView; +import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; +import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; + import okhttp3.OkHttpClient; import java.util.List; +import java.util.Set; +import java.util.HashSet; import java.util.concurrent.TimeUnit; +import static java.util.Collections.singletonList; -public class Main { +import com.google.common.collect.ImmutableMap; - private static final int PAGEVIEW_COUNT = 10; +public class Main { public static String getUrlFromArgs(String[] args) { if (args == null || args.length < 1) { @@ -53,9 +58,10 @@ public static HttpClientAdapter getClient(String url) { } public static void main(String[] args) { + Set failedEventIds = new HashSet(); String collectorEndpoint = getUrlFromArgs(args); - System.out.println("Sending " + PAGEVIEW_COUNT + " events to " + collectorEndpoint); + System.out.println("Sending events to " + collectorEndpoint); // get the client adapter // this is used by the Java tracker to transmit events to the collector @@ -67,41 +73,107 @@ public static void main(String[] args) { String namespace = "demo"; // build an emitter, this is used by the tracker to batch and schedule transmission of events - Emitter emitter = BatchEmitter.builder() + BatchEmitter emitter = BatchEmitter.builder() .httpClientAdapter(okHttpClientAdapter) .requestCallback(new RequestCallback() { // let us know on successes (may be called multiple times) @Override - public void onSuccess(int successCount) { + public synchronized void onSuccess(int successCount) { System.out.println("Successfully sent " + successCount + " events"); } // let us know if something has gone wrong (may be called multiple times) @Override - public void onFailure(int successCount, List failedEvents) { + public synchronized void onFailure(int successCount, List failedEvents) { System.err.println("Successfully sent " + successCount + " events; failed to send " + failedEvents.size() + " events"); } }) - .bufferSize(1) // send an event every time one is given (no batching). In production this number should be higher, depending on the size/event volume + .bufferSize(4) // send an event every time one is given (no batching). In production this number should be higher, depending on the size/event volume .build(); // now we have the emitter, we need a tracker to turn our events into something a Snowplow collector can understand - Tracker tracker = new Tracker.TrackerBuilder(emitter, namespace, appId) - .base64(true) - .platform(DevicePlatform.ServerSideApp) - .build(); + final Tracker tracker = new Tracker.TrackerBuilder(emitter, namespace, appId) + .base64(true) + .platform(DevicePlatform.ServerSideApp) + .build(); - for (int i = 0; i < PAGEVIEW_COUNT; i++) { - // This is a sample page view event, many other event types (such as self-describing events) are available - PageView pageViewEvent = PageView.builder() - .pageTitle("Hello world " + i) - .pageUrl("https://www.snowplowanalytics.com") - .referrer("https://www.google.com") - .build(); - - tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow - } + // This is an example of a custom context + List contexts = singletonList( + new SelfDescribingJson( + "iglu:com.snowplowanalytics.iglu/anything-c/jsonschema/1-0-0", + ImmutableMap.of("foo", "bar"))); + + // This is a sample page view event, many other event types (such as self-describing events) are available + PageView pageViewEvent = PageView.builder() + .pageTitle("Snowplow Analytics") + .pageUrl("https://www.snowplowanalytics.com") + .referrer("https://www.google.com") + .customContext(contexts) + .build(); + + tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow + + EcommerceTransactionItem item = EcommerceTransactionItem.builder() + .itemId("order_id") + .sku("sku") + .price(1.0) + .quantity(2) + .name("name") + .category("category") + .currency("currency") + .customContext(contexts) + .build(); + + EcommerceTransaction ecommerceTransaction = EcommerceTransaction.builder() + .orderId("order_id") + .totalValue(1.0) + .affiliation("affiliation") + .taxValue(2.0) + .shipping(3.0) + .city("city") + .state("state") + .country("country") + .currency("currency") + .items(item) // EcommerceTransactionItem events are added to a parent EcommerceTransaction + .customContext(contexts) + .build(); + + tracker.track(ecommerceTransaction); // This will track two events + + // This is an example of a custom "Unsutrcutred" event based on a schema + Unstructured unstructured = Unstructured.builder() + .eventData(new SelfDescribingJson( + "iglu:com.snowplowanalytics.iglu/anything-a/jsonschema/1-0-0", + ImmutableMap.of("foo", "bar") + )) + .customContext(contexts) + .build(); + + tracker.track(unstructured); + + // This is an example of a ScreenView event which will be translated into an Unstructured event + ScreenView screenView = ScreenView.builder() + .name("name") + .id("id") + .customContext(contexts) + .build(); + + tracker.track(screenView); + + // This is an example of a Timing event which will be translated into an Unstructured event + Timing timing = Timing.builder() + .category("category") + .label("label") + .variable("variable") + .timing(10) + .customContext(contexts) + .build(); + + tracker.track(timing); + // Will close all threads and force send remaining events + // should be 1 left to flush, as we send 5 events with a bufferSize of 4 + emitter.close(); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index 8b06b116..f591c93c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -44,6 +44,14 @@ private Subject(SubjectBuilder builder) { this.setDomainUserId(builder.domainUserId); } + /** + * Creates a new {@link Subject} object based on the map of another {@link Subject} object. + * @param subject The subject from which the map is copied. + */ + public Subject(Subject subject){ + this.standardPairs.putAll(subject.getSubject()); + } + /** * Builder for the Subject */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index be31db39..0f8cdf0e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -12,29 +12,18 @@ */ package com.snowplowanalytics.snowplow.tracker; -// Java -import java.util.*; - -// Google import com.google.common.base.Preconditions; -// This library -import com.snowplowanalytics.snowplow.tracker.constants.Constants; -import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; -import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; public class Tracker { - private final String trackerVersion = Version.TRACKER; private Emitter emitter; private Subject subject; - private String appId; - private String namespace; - private DevicePlatform platform; - private boolean base64Encoded; + private final TrackerParameters parameters; /** * Creates a new Snowplow Tracker. @@ -50,12 +39,9 @@ private Tracker(TrackerBuilder builder) { Preconditions.checkArgument(!builder.namespace.isEmpty(), "namespace cannot be empty"); Preconditions.checkArgument(!builder.appId.isEmpty(), "appId cannot be empty"); + this.parameters = new TrackerParameters(builder.appId, builder.platform, builder.namespace, Version.TRACKER, builder.base64Encoded); this.emitter = builder.emitter; - this.namespace = builder.namespace; - this.appId = builder.appId; this.subject = builder.subject; - this.platform = builder.platform; - this.base64Encoded = builder.base64Encoded; } /** @@ -137,44 +123,6 @@ public void setSubject(Subject subject) { this.subject = subject; } - /** - * Sets the Trackers platform, defaults to a - * Server Side Application. - * - * @param platform the DevicePlatform - */ - public void setPlatform(DevicePlatform platform) { - this.platform = platform; - } - - /** - * Sets whether to base64 Encode custom contexts - * and unstructured events - * - * @param base64Encoded a boolean truth - */ - public void setBase64Encoded(boolean base64Encoded) { - this.base64Encoded = base64Encoded; - } - - /** - * Sets a new Application ID - * - * @param appId the new application id - */ - public void setAppId(String appId) { - this.appId = appId; - } - - /** - * Sets a new Tracker Namespace - * - * @param namespace the new tracker namespace - */ - public void setNamespace(String namespace) { - this.namespace = namespace; - } - // --- Getters /** @@ -195,49 +143,46 @@ public Subject getSubject() { * @return the tracker version that was set */ public String getTrackerVersion() { - return this.trackerVersion; + return this.parameters.getTrackerVersion(); } /** * @return the trackers namespace */ public String getNamespace() { - return this.namespace; + return this.parameters.getNamespace(); } /** * @return the trackers set Application ID */ public String getAppId() { - return this.appId; + return this.parameters.getAppId(); } /** * @return the base64 setting of the tracker */ public boolean getBase64Encoded() { - return this.base64Encoded; + return this.parameters.getBase64Encoded(); } /** * @return the Tracker platform */ public DevicePlatform getPlatform() { - return this.platform; + return this.parameters.getPlatform(); } - // --- Event Tracking Functions - /** - * Used for either Tracking a custom TrackerPayload or - * for re-sending a failed event. - * - * @param payload the payload to track + * @return the wrapper containing the Tracker parameters */ - public void track(TrackerPayload payload) { - this.emitter.emit(payload); + public TrackerParameters getParameters() { + return this.parameters; } + // --- Event Tracking Functions + /** * Handles tracking the different types of events that * the Tracker can encounter. @@ -245,89 +190,7 @@ public void track(TrackerPayload payload) { * @param event the event to track */ public void track(Event event) { - List context = event.getContext(); - Subject subject = event.getSubject(); - - // Figure out what type of event it is and track it! - Class eClass = event.getClass(); - if (eClass.equals(PageView.class) || eClass.equals(Structured.class)) { - this.addTrackerPayload((TrackerPayload) event.getPayload(), context, subject); - } else if (eClass.equals(EcommerceTransaction.class)) { - this.addTrackerPayload((TrackerPayload) event.getPayload(), context, subject); - - // Track each item individually - EcommerceTransaction ecommerceTransaction = (EcommerceTransaction) event; - for(EcommerceTransactionItem item : ecommerceTransaction.getItems()) { - item.setTimestamp(ecommerceTransaction.getTimestamp()); - this.addTrackerPayload(item.getPayload(), item.getContext(), item.getSubject()); - } - } else if (eClass.equals(Unstructured.class)) { - - // Need to set the Base64 rule for Unstructured events - Unstructured unstructured = (Unstructured) event; - unstructured.setBase64Encode(base64Encoded); - this.addTrackerPayload(unstructured.getPayload(), context, subject); - } else if (eClass.equals(Timing.class) || eClass.equals(ScreenView.class)) { - - // These are wrapper classes for Unstructured events; need to create Unstructured - // events from them and resend. - this.track(Unstructured.builder() - .eventData((SelfDescribingJson) event.getPayload()) - .customContext(context) - .deviceCreatedTimestamp(event.getDeviceCreatedTimestamp()) - .trueTimestamp(event.getTrueTimestamp()) - .eventId(event.getEventId()) - .subject(subject) - .build()); - } - } - - // --- Helpers - - /** - * Builds and Adds a finalised payload which is ready for sending. - * - * @param payload The raw event Payload - * @param contexts Custom context for the event - * @param eventSubject An optional event specific Subject - */ - private void addTrackerPayload(TrackerPayload payload, List contexts, Subject eventSubject) { - - // Add default parameters to the payload - payload.add(Parameter.PLATFORM, platform.toString()); - payload.add(Parameter.APP_ID, this.appId); - payload.add(Parameter.NAMESPACE, this.namespace); - payload.add(Parameter.TRACKER_VERSION, this.trackerVersion); - - // Build the final context and add it to the payload - if (contexts != null && contexts.size() > 0) { - SelfDescribingJson envelope = getFinalContext(contexts); - payload.addMap(envelope.getMap(), this.base64Encoded, Parameter.CONTEXT_ENCODED, Parameter.CONTEXT); - } - - // Add subject if available - if (eventSubject != null) { - payload.addMap(new HashMap<>(eventSubject.getSubject())); - } else if (this.subject != null) { - payload.addMap(new HashMap<>(this.subject.getSubject())); - } - - // Send the event! - this.emitter.emit(payload); - } - - /** - * Builds the final event context. - * - * @param contexts the base event context - * @return the final event context json with - * many contexts inside - */ - private SelfDescribingJson getFinalContext(List contexts) { - List contextMaps = new LinkedList<>(); - for (SelfDescribingJson selfDescribingJson : contexts) { - contextMaps.add(selfDescribingJson.getMap()); - } - return new SelfDescribingJson(Constants.SCHEMA_CONTEXTS, contextMaps); + // Emit the event + this.emitter.emit(new TrackerEvent(event, this.parameters, this.subject)); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index a59c7ac0..ebcfdf3d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -12,23 +12,17 @@ */ package com.snowplowanalytics.snowplow.tracker; -// Java import java.nio.charset.Charset; import java.util.*; import java.net.URL; import java.net.URLEncoder; -// Jackson import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -// Slf4j import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// Apache -import static org.apache.commons.codec.binary.Base64.encodeBase64String; - /** * Provides basic Utilities for the Snowplow Tracker. */ @@ -111,7 +105,7 @@ public static String getTimezone() { * @return a Base64 encoded string */ public static String base64Encode(String string, Charset charset) { - return encodeBase64String(string.getBytes(charset)); + return Base64.getEncoder().encodeToString(string.getBytes(charset)); } /** @@ -121,7 +115,7 @@ public static String base64Encode(String string, Charset charset) { * @param map the map to process into a JSON String * @return the final JSON String */ - public static String mapToJSONString(Map map) { + public static String mapToJSONString(Map map) { String jString = ""; try { jString = objectMapper.writeValueAsString(map); @@ -137,7 +131,7 @@ public static String mapToJSONString(Map map) { * @param map The map to convert * @return the QueryString ready for sending */ - public static String mapToQueryString(Map map) { + public static String mapToQueryString(Map map) { StringBuilder sb = new StringBuilder(); for (String key : map.keySet()) { if (sb.length() > 0) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index e3fd3863..9e483f62 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -12,18 +12,14 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -// Java -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -// Google import com.google.common.base.Preconditions; -// This library import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; /** * AbstractEmitter class which contains common elements to @@ -34,8 +30,6 @@ public abstract class AbstractEmitter implements Emitter { protected HttpClientAdapter httpClientAdapter; protected RequestCallback requestCallback; protected ExecutorService executor; - protected List buffer = new ArrayList<>(); - protected int bufferSize = 1; public static abstract class Builder> { @@ -102,28 +96,24 @@ protected AbstractEmitter(final Builder builder) { } /** - * Adds a payload to the buffer and checks whether we have reached the buffer - * limit yet. + * Adds an event to the buffer * - * @param payload an event payload + * @param event an event */ @Override - public abstract void emit(TrackerPayload payload); + public abstract void emit(TrackerEvent event); /** - * Customize the emitter buffer size to any valid integer greater than zero. - - * Will only effect the BatchEmitter + * Customize the emitter buffer size to any valid integer greater than zero. + * Has no effect on SimpleEmitter * * @param bufferSize number of events to collect before sending */ @Override - public void setBufferSize(final int bufferSize) { - Preconditions.checkArgument(bufferSize > 0, "bufferSize must be greater than 0"); - this.bufferSize = bufferSize; - } + public abstract void setBufferSize(final int bufferSize); /** - * When the buffer limit is reached sending of the buffer is initiated. + * Removes all events from the buffer and sends them */ @Override public abstract void flushBuffer(); @@ -134,19 +124,15 @@ public void setBufferSize(final int bufferSize) { * @return the buffer size */ @Override - public int getBufferSize() { - return this.bufferSize; - } + public abstract int getBufferSize(); /** - * Returns the List of Payloads that are in the buffer. + * Returns List of Events that are in the buffer. * - * @return the buffer payloads + * @return the buffered events */ @Override - public List getBuffer() { - return this.buffer; - } + public abstract List getBuffer(); /** * Sends a runnable to the executor service. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index fcfda15f..c5024f9a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -12,34 +12,44 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -// Java import java.io.Closeable; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; -// Google import com.google.common.base.Preconditions; - -// Slf4j -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -// This library import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * An emitter that emit a batch of events in a single call + * An emitter that emit a batch of events in a single call * It uses the post method of under-laying http adapter */ public class BatchEmitter extends AbstractEmitter implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); + private final Thread bufferConsumer; + private boolean isClosing = false; + + private int bufferSize = 1; + + // Queue for immediate buffering of events + private final BlockingQueue eventBuffer = new LinkedBlockingQueue<>(); + + // Queue for storing events until bufferSize is reached + private final BlockingQueue eventsToSend = new LinkedBlockingQueue<>(); + private final long closeTimeout = 5; public static abstract class Builder> extends AbstractEmitter.Builder { @@ -78,37 +88,113 @@ protected BatchEmitter(final Builder builder) { Preconditions.checkArgument(builder.bufferSize > 0, "bufferSize must be greater than 0"); this.bufferSize = builder.bufferSize; + + bufferConsumer = new Thread(getBufferConsumerRunnable()); + bufferConsumer.start(); } /** - * Adds a payload to the buffer and checks whether we have reached the buffer - * limit yet. + * Adds a TrackerEvent to the concurrent queue buffer * - * @param payload an event payload + * @param event an event */ @Override - public synchronized void emit(final TrackerPayload payload) { - buffer.add(payload); - if (buffer.size() >= bufferSize) { - flushBuffer(); + public void emit(final TrackerEvent event) { + boolean result = eventBuffer.offer(event); // Add to buffer and quickly return back to application + + if (!result) { + LOGGER.error("Unable to add event to emitter, emitter buffer is full"); } } - /** - * When the buffer limit is reached sending of the buffer is initiated. + /* + * Forces the events currently in the buffer to be sent */ + @Override public void flushBuffer() { - execute(getRequestRunnable(buffer)); - buffer = new ArrayList<>(); + // Drain immediate event buffer + while (true) { + TrackerEvent event = eventBuffer.poll(); + if (event == null) { + break; + } else { + eventsToSend.offer(event); + } + } + + drainBufferAndSend(); + } + + /** + * Returns List of Events that are in the buffer. + * + * @return the buffered events + */ + @Override + public List getBuffer() { + return eventsToSend.stream().collect(Collectors.toList()); + } + + /** + * Customize the emitter buffer size to any valid integer greater than zero. + * + * @param bufferSize number of events to collect before sending + */ + @Override + public void setBufferSize(final int bufferSize) { + Preconditions.checkArgument(bufferSize > 0, "bufferSize must be greater than 0"); + this.bufferSize = bufferSize; + } + + /** + * Gets the Emitter Buffer Size + * + * @return the buffer size + */ + @Override + public int getBufferSize() { + return this.bufferSize; + } + + /** + * Returns a Consumer for the concurrent queue buffer + * Consumes events onto another queue to be sent when bufferSize is reached + * + * @return the new Runnable object + */ + private Runnable getBufferConsumerRunnable() { + return new Runnable() { + @Override + public void run() { + while (true) { + try { + eventsToSend.put(eventBuffer.take()); + if (eventsToSend.size() >= bufferSize) { + drainBufferAndSend(); + } + } catch (InterruptedException ex) { + if (isClosing) { + return; + } + } + } + } + }; + } + + private void drainBufferAndSend() { + List events = new ArrayList<>(); + eventsToSend.drainTo(events); + execute(getRequestRunnable(events)); } /** * Returns a Runnable POST Request operation * * @param buffer the event buffer to be sent - * @return the new Callable object + * @return the new Runnable object */ - private Runnable getRequestRunnable(final List buffer) { + private Runnable getRequestRunnable(final List buffer) { return new Runnable() { @Override public void run() { @@ -133,7 +219,8 @@ public void run() { // Send the callback if available if (requestCallback != null) { if (failure != 0) { - requestCallback.onFailure(success, buffer); + requestCallback.onFailure(success, + buffer.stream().map(te -> te.getEvent()).collect(Collectors.toList())); } else { requestCallback.onSuccess(success); } @@ -145,13 +232,19 @@ public void run() { /** * Constructs the SelfDescribingJson to be sent to the endpoint * + * @param buffer the event buffer * @return the constructed POST payload */ - private SelfDescribingJson getFinalPost(final List buffer) { - final List toSendPayloads = new ArrayList<>(); - for (final TrackerPayload payload : buffer) { - payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); - toSendPayloads.add(payload.getMap()); + private SelfDescribingJson getFinalPost(final List buffer) { + final List> toSendPayloads = new ArrayList<>(); + final String sentTimestamp = Long.toString(System.currentTimeMillis()); + + for (TrackerEvent event : buffer) { + List payloads = event.getTrackerPayloads(); + for (TrackerPayload payload : payloads) { + payload.add(Parameter.DEVICE_SENT_TIMESTAMP, sentTimestamp); + toSendPayloads.add(payload.getMap()); + } } return new SelfDescribingJson(Constants.SCHEMA_PAYLOAD_DATA, toSendPayloads); @@ -162,7 +255,12 @@ private SelfDescribingJson getFinalPost(final List buffer) { */ @Override public void close() { - flushBuffer(); + isClosing = true; + + bufferConsumer.interrupt(); // Kill buffer consumer + flushBuffer(); // Attempt to send all reminaing events + + //Shutdown executor threadpool if (executor != null) { executor.shutdown(); try { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index 0eab9e1c..e1cd535f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -12,23 +12,22 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -// This library -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; - import java.util.List; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; + /** * Emitter interface. */ public interface Emitter { /** - * Adds a payload to the buffer and checks whether + * Adds an event to the buffer and checks whether * we have reached the buffer limit yet. * - * @param payload an event payload + * @param event an event to be emitted */ - void emit(TrackerPayload payload); + void emit(TrackerEvent event); /** * Customize the emitter buffer size to any valid integer @@ -57,9 +56,9 @@ public interface Emitter { int getBufferSize(); /** - * Returns the List of Payloads that are in the buffer. + * Returns the List of Events that are in the buffer. * - * @return the buffer payloads + * @return the buffer events */ - List getBuffer(); + List getBuffer(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java index 00baeee4..deb9cf05 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java @@ -12,11 +12,9 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -// Java import java.util.List; -// This library -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.events.Event; /** * Provides a callback interface for reporting counts of successfully sent @@ -34,10 +32,10 @@ public interface RequestCallback { /** * If all/some events failed then the count of successful - * events is returned along with all the failed Payloads. + * events is returned along with all the failed Events. * * @param successCount the successful count - * @param failedEvents the list of failed payloads + * @param failedEvents the list of failed events */ - void onFailure(int successCount, List failedEvents); + void onFailure(int successCount, List failedEvents); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 823801bd..19c39c18 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -12,17 +12,16 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -// Java import java.util.ArrayList; import java.util.List; -// Slf4j import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// This library +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; +import com.snowplowanalytics.snowplow.tracker.events.Event; /** * An emitter which sends events as soon as they are received via @@ -54,18 +53,20 @@ protected SimpleEmitter(final Builder builder) { } /** - * Adds a payload to the buffer and instantly sends it + * Adds an event to the buffer and instantly sends it * - * @param payload an event payload + * @param event an event */ @Override - public void emit(final TrackerPayload payload) { - execute(getRequestRunnable(payload)); + public void emit(final TrackerEvent event) { + execute(getRequestRunnable(event)); } /** - * When the buffer limit is reached sending of the buffer is initiated. + * Sends buffered events, but SimpleEmitter does not buffer events + * So has no effect */ + @Override public void flushBuffer() { // Do nothing! } @@ -73,32 +74,37 @@ public void flushBuffer() { /** * Returns a Runnable GET Request operation * - * @param payload the event to be sent + * @param event the event to be sent * @return the new Callable object */ - private Runnable getRequestRunnable(final TrackerPayload payload) { + private Runnable getRequestRunnable(final TrackerEvent event) { return new Runnable() { @Override public void run() { - payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); - final int code = httpClientAdapter.get(payload); - - // Process results int success = 0; int failure = 0; - if (!isSuccessfulSend(code)) { - LOGGER.error("SimpleEmitter failed to send {} events: code: {}", 1, code); - failure += 1; - } else { - LOGGER.debug("SimpleEmitter successfully sent {} events: code: {}", 1, code); - success += 1; + + List payloads = event.getTrackerPayloads(); + + for (TrackerPayload payload : payloads) { + payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); + final int code = httpClientAdapter.get(payload); + + // Process results + if (!isSuccessfulSend(code)) { + LOGGER.error("SimpleEmitter failed to send {} events: code: {}", 1, code); + failure += 1; + } else { + LOGGER.debug("SimpleEmitter successfully sent {} events: code: {}", 1, code); + success += 1; + } } // Send the callback if available if (requestCallback != null) { if (failure != 0) { - final List buffer = new ArrayList<>(); - buffer.add(payload); + final List buffer = new ArrayList<>(); + buffer.add(event.getEvent()); requestCallback.onFailure(success, buffer); } else { requestCallback.onSuccess(success); @@ -107,4 +113,38 @@ public void run() { } }; } + + /** + * Returns List of Events that are in the buffer. + * Always empty for SimpleEmitter + * + * @return the empty buffer + */ + @Override + public List getBuffer() { + return new ArrayList<>(); + } + + /** + * Customize the emitter buffer size to any valid integer greater than zero. + * Has no effect on SimpleEmitter + * + * @param bufferSize number of events to collect before sending + */ + @Override + public void setBufferSize(final int bufferSize) { + if (bufferSize != 1) { + LOGGER.debug("Noop. SimpleEmitter buffer size must always be 1."); + } + } + + /** + * Gets the Emitter Buffer Size - Will always be 1 for SimpleEmitter + * + * @return the buffer size + */ + @Override + public int getBufferSize() { + return 1; + } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 2a65d54a..0c40230b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -12,10 +12,8 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -// Java import java.util.List; -// This library import com.snowplowanalytics.snowplow.tracker.Subject; import com.snowplowanalytics.snowplow.tracker.payload.Payload; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index d3210edc..034bd395 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -12,10 +12,8 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -// Google import com.google.common.base.Preconditions; -// This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index 3c289575..5a5f34cd 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -34,13 +34,13 @@ public static abstract class Builder> extends AbstractEvent private SelfDescribingJson eventData; /** - * @param eventData The properties of the event. Has two field: + * @param selfDescribingJson The properties of the event. Has two field: * A "data" field containing the event properties and * A "schema" field identifying the schema against which the data is validated * @return itself */ - public T eventData(SelfDescribingJson eventData) { - this.eventData = eventData; + public T eventData(SelfDescribingJson selfDescribingJson) { + this.eventData = selfDescribingJson; return self(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index 0468aabd..ebd3c96c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -12,13 +12,8 @@ */ package com.snowplowanalytics.snowplow.tracker.http; -// Java -import java.util.Map; - -// Google import com.google.common.base.Preconditions; -// This library import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.Utils; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -94,7 +89,6 @@ public int post(SelfDescribingJson payload) { * @param payload the TrackerPayload to send */ @Override - @SuppressWarnings("unchecked") public int get(TrackerPayload payload) { String url = this.url + "/i?" + Utils.mapToQueryString(payload.getMap()); return doGet(url); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 97e9d5ab..1499d294 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -12,26 +12,18 @@ */ package com.snowplowanalytics.snowplow.tracker.http; -// Java -import java.util.Map; - -// Google import com.google.common.base.Preconditions; -// Apache import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -// Slf4j import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// This library import com.snowplowanalytics.snowplow.tracker.constants.Constants; /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java index 315ee8a0..8413a0b8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java @@ -12,7 +12,6 @@ */ package com.snowplowanalytics.snowplow.tracker.payload; -// Java import java.util.Map; /** @@ -48,14 +47,14 @@ public interface Payload { * @param typeEncoded The key that would be set if the encoding option was set to true * @param typeNotEncoded They key that would be set if the encoding option was set to false */ - void addMap(Map map, boolean base64Encoded, String typeEncoded, String typeNotEncoded); + void addMap(Map map, boolean base64Encoded, String typeEncoded, String typeNotEncoded); /** * Returns the Payload as a HashMap. * * @return A HashMap */ - Map getMap(); + Map getMap(); /** * Returns the byte size of a payload. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index 18cc98d8..e7f88c85 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -12,18 +12,14 @@ */ package com.snowplowanalytics.snowplow.tracker.payload; -// Java import java.util.LinkedHashMap; import java.util.Map; -// Google import com.google.common.base.Preconditions; -// Slf4j import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// This library import com.snowplowanalytics.snowplow.tracker.Utils; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; @@ -157,7 +153,7 @@ public void addMap(Map map) { @Deprecated @Override - public void addMap(Map map, boolean base64Encoded, String typeEncoded, String typeNotEncoded) { + public void addMap(Map map, boolean base64Encoded, String typeEncoded, String typeNotEncoded) { LOGGER.info("Payload: addMap(Map, boolean, String, String) method called - Doing nothing."); } @@ -167,7 +163,7 @@ public void addMap(Map map, boolean base64Encoded, String typeEncoded, String ty * @return A Map of all the key-value entries */ @Override - public Map getMap() { + public Map getMap() { return payload; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java new file mode 100644 index 00000000..d6ef6eef --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2020-2020 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.payload; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.snowplowanalytics.snowplow.tracker.Subject; +import com.snowplowanalytics.snowplow.tracker.constants.Constants; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; +import com.snowplowanalytics.snowplow.tracker.events.*; + +/** + * A TrackerEvent which allows the TrackerPayload to be filled later. The payload will be + * filled by the Emitter in the Emitter thread, using the getTrackerPayload() method. + */ +public class TrackerEvent { + + private final Event event; + private final TrackerParameters parameters; + private final Subject subject; + + public TrackerEvent(final Event event, final TrackerParameters parameters, final Subject subject) { + this.event = event; + this.parameters = parameters; + this.subject = subject; + } + + /** + * Returns the {@link Event} + * + * @return The {@link Event} + */ + public Event getEvent() { + return this.event; + } + + /** + * Converts a {@link Event} to a list of {@link TrackerPayload} and caches the values. + * Returns a list as some Events contain nested payloads (e.g. {@link EcommerceTransaction}) + * Adds fields to the {@link TrackerPayload} based on the type of the {@link Event}. + * + * @return The populated TrackerPayloads + */ + public List getTrackerPayloads() { + final List payloads = new ArrayList<>(); + final List contexts = event.getContext(); + final Subject subject = event.getSubject(); + + // Figure out what type of event it is + final Class eventClass = event.getClass(); + + if (eventClass.equals(Unstructured.class)) { + + // Need to set the Base64 rule for Unstructured events + final Unstructured unstructured = (Unstructured) event; + unstructured.setBase64Encode(this.parameters.getBase64Encoded()); + TrackerPayload payload = unstructured.getPayload(); + addTrackerParameters(payload); + addContextsAndSubject(contexts, subject, payload); + payloads.add(payload); + } else if (eventClass.equals(Timing.class) || eventClass.equals(ScreenView.class)) { + + // These are wrapper classes for Unstructured events; need to create + // Unstructured events from them and resend. + final Unstructured unstructured = Unstructured.builder() + .eventData((SelfDescribingJson) event.getPayload()) + .customContext(contexts) + .deviceCreatedTimestamp(event.getDeviceCreatedTimestamp()) + .trueTimestamp(event.getTrueTimestamp()) + .eventId(event.getEventId()) + .subject(subject) + .build(); + + unstructured.setBase64Encode(this.parameters.getBase64Encoded()); + TrackerPayload payload = unstructured.getPayload(); + addTrackerParameters(payload); + addContextsAndSubject(contexts, subject, payload); + payloads.add(payload); + } else if (eventClass.equals(EcommerceTransaction.class)) { + + final EcommerceTransaction ecommerceTransaction = (EcommerceTransaction) event; + TrackerPayload payload = ecommerceTransaction.getPayload(); + addTrackerParameters(payload); + addContextsAndSubject(contexts, subject, payload); + payloads.add(payload); + + // Track each item individually + for (final EcommerceTransactionItem item : ecommerceTransaction.getItems()) { + + item.setDeviceCreatedTimestamp(ecommerceTransaction.getDeviceCreatedTimestamp()); + TrackerPayload itemPayload = item.getPayload(); + addTrackerParameters(itemPayload); + addContextsAndSubject(item.getContext(), item.getSubject(), itemPayload); + payloads.add(itemPayload); + } + } else { + + // For all other events, simply get the payload + TrackerPayload payload = (TrackerPayload) event.getPayload(); + addTrackerParameters(payload); + addContextsAndSubject(contexts, subject, payload); + payloads.add(payload); + } + + return payloads; + } + + /** + * Adds the context and subject to the event payload + * + * @param contexts the base event context - can be null or empty + * @param subject the event subject - can be null + * @param payload the payload to add the contexts and subjects to + */ + private void addContextsAndSubject(final List contexts, final Subject subject, TrackerPayload payload) { + // Build the final context and add it to the payload + if (contexts != null && contexts.size() > 0) { + SelfDescribingJson envelope = getFinalContext(contexts); + payload.addMap(envelope.getMap(), this.parameters.getBase64Encoded(), Parameter.CONTEXT_ENCODED, Parameter.CONTEXT); + } + + // Add subject if available + if (subject != null) { + payload.addMap(new HashMap<>(subject.getSubject())); + } else if (this.subject != null) { + payload.addMap(new HashMap<>(this.subject.getSubject())); + } + } + + /** + * Builds the final event context. + * + * @param contexts the base event context + * @return the final event context json with many contexts inside + */ + private SelfDescribingJson getFinalContext(List contexts) { + List> contextMaps = new LinkedList<>(); + for (SelfDescribingJson selfDescribingJson : contexts) { + contextMaps.add(selfDescribingJson.getMap()); + } + return new SelfDescribingJson(Constants.SCHEMA_CONTEXTS, contextMaps); + } + + private void addTrackerParameters(TrackerPayload payload) { + payload.add(Parameter.PLATFORM, this.parameters.getPlatform().toString()); + payload.add(Parameter.APP_ID, this.parameters.getAppId()); + payload.add(Parameter.NAMESPACE, this.parameters.getNamespace()); + payload.add(Parameter.TRACKER_VERSION, this.parameters.getTrackerVersion()); + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java new file mode 100644 index 00000000..f94abbf8 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020-2020 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.payload; + +import com.snowplowanalytics.snowplow.tracker.DevicePlatform; + +/** + * A TrackerEvent which allows the TrackerPayload to be filled later. The + * payload will be filled by the Emitter in the Emitter thread, using the + * getTrackerPayload() method. + */ +public class TrackerParameters { + + private final String trackerVersion; + private final String appId; + private final DevicePlatform platform; + private final String namespace; + private final boolean base64Encoded; + + public TrackerParameters(String appId, DevicePlatform platform, String namespace, String trackerVersion, + boolean base64Encoded) { + this.appId = appId; + this.platform = platform; + this.namespace = namespace; + this.trackerVersion = trackerVersion; + this.base64Encoded = base64Encoded; + } + + public boolean getBase64Encoded() { + return base64Encoded; + } + + public String getTrackerVersion() { + return trackerVersion; + } + + public String getNamespace() { + return namespace; + } + + public String getAppId() { + return appId; + } + + public DevicePlatform getPlatform() { + return platform; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 08342b37..3a81ff1c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -12,16 +12,13 @@ */ package com.snowplowanalytics.snowplow.tracker.payload; -// Java import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; -// Slf4j import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// This library import com.snowplowanalytics.snowplow.tracker.Utils; /** @@ -31,23 +28,22 @@ public class TrackerPayload implements Payload { private static final Logger LOGGER = LoggerFactory.getLogger(TrackerPayload.class); - private final LinkedHashMap payload = new LinkedHashMap<>(); + protected final Map payload = new LinkedHashMap<>(); /** - * Add a key-value pair to the payload: - * - Checks that the key is not null or empty - * - Checks that the value is not null or empty + * Add a key-value pair to the payload: - Checks that the key is not null or + * empty - Checks that the value is not null or empty * - * @param key The parameter key + * @param key The parameter key * @param value The parameter value as a String */ @Override - public void add(String key, String value) { + public void add(final String key, final String value) { if (key == null || key.isEmpty()) { LOGGER.error("Invalid key detected: {}", key); return; } - if (value == null || value.isEmpty()) { + if (value == null || value.isEmpty()) { LOGGER.info("null or empty value detected: {}", value); return; } @@ -56,40 +52,40 @@ public void add(String key, String value) { } /** - * Add all the mappings from the specified map. The effect is the equivalent to that of calling: - * - add(String key, String value) for each key value pair. + * Add all the mappings from the specified map. The effect is the equivalent to + * that of calling: - add(String key, String value) for each key value pair. * * @param map Key-Value pairs to be stored in this payload */ @Override - public void addMap(Map map) { + public void addMap(final Map map) { if (map == null) { LOGGER.debug("Map passed in is null, returning without adding map."); return; } LOGGER.debug("Adding new map: {}", map); - for (Map.Entry entry : map.entrySet()) { + for (final Map.Entry entry : map.entrySet()) { add(entry.getKey(), entry.getValue()); } } /** - * Add a map to the Payload with a key dependent on the base 64 encoding option you choose using the - * two keys provided. + * Add a map to the Payload with a key dependent on the base 64 encoding option + * you choose using the two keys provided. * - * @param map Map to be converted to a String and stored as a value - * @param base64Encoded The option you choose to encode the data - * @param typeEncoded The key that would be set if the encoding option was set to true + * @param map Map to be converted to a String and stored as a value + * @param base64Encoded The option you choose to encode the data + * @param typeEncoded The key that would be set if the encoding option was set to true * @param typeNotEncoded They key that would be set if the encoding option was set to false */ @Override - public void addMap(Map map, boolean base64Encoded, String typeEncoded, String typeNotEncoded) { + public void addMap(final Map map, final boolean base64Encoded, final String typeEncoded, final String typeNotEncoded) { if (map == null) { LOGGER.debug("Map passed in is null, returning nothing."); return; } - String mapString = Utils.mapToJSONString(map); + final String mapString = Utils.mapToJSONString(map); LOGGER.debug("Adding new map: {}", map); if (base64Encoded) { @@ -105,7 +101,7 @@ public void addMap(Map map, boolean base64Encoded, String typeEncoded, String ty * @return A Map of all the key-value entries */ @Override - public Map getMap() { + public Map getMap() { return payload; } @@ -120,8 +116,8 @@ public long getByteSize() { } /** - * Returns the Payload as a string. This is essentially the toString from the ObjectNode used - * to store the Payload. + * Returns the Payload as a string. This is essentially the toString from the + * ObjectNode used to store the Payload. * * @return A string value of the Payload. */ diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index bbc93ced..713aef74 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -12,32 +12,28 @@ */ package com.snowplowanalytics.snowplow.tracker; -// Java import java.util.*; import static java.util.Collections.singletonList; -// JUnit import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -// Google import com.google.common.collect.ImmutableMap; -// Mockito import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.*; -// This library import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; @RunWith(MockitoJUnitRunner.class) public class TrackerTest { @@ -49,7 +45,7 @@ public class TrackerTest { Emitter emitter; @Captor - ArgumentCaptor captor; + ArgumentCaptor captor; Tracker tracker; private List contexts; @@ -102,10 +98,12 @@ public void testEcommerceEvent() { .build()); // Then - verify(emitter, times(2)).emit(captor.capture()); - List allValues = captor.getAllValues(); + verify(emitter, times(1)).emit(captor.capture()); + List allValues = captor.getAllValues(); - Map result1 = allValues.get(0).getMap(); + assertEquals(allValues.get(0).getTrackerPayloads().size(), 2); + + Map result1 = allValues.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("e", "tr") .put("tr_cu", "currency") @@ -128,7 +126,7 @@ public void testEcommerceEvent() { .put("tr_st", "state") .build(), result1); - Map result2 = allValues.get(1).getMap(); + Map result2 = allValues.get(0).getTrackerPayloads().get(1).getMap(); assertEquals(ImmutableMap.builder() .put("ti_nm", "name") .put("ti_id", "order_id") @@ -167,7 +165,7 @@ public void testUnstructuredEventWithContext() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -198,7 +196,7 @@ public void testUnstructuredEventWithoutContext() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -227,7 +225,7 @@ public void testUnstructuredEventWithoutTrueTimestamp() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -256,7 +254,7 @@ public void testTrackPageView() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -274,6 +272,63 @@ public void testTrackPageView() { .build(), result); } + @Test + public void testTrackTwoEvents() { + // When + tracker.track(PageView.builder() + .pageUrl("url") + .pageTitle("title") + .referrer("referer") + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) + .eventId("9783090a-dace-4c85-a75c-933b4596a6c5") + .build()); + + tracker.track(PageView.builder() + .pageUrl("url") + .pageTitle("title") + .referrer("referer") + .deviceCreatedTimestamp(123456) + .trueTimestamp(456789L) + .eventId("39139d43-ea13-4163-8559-adea258bf9c4") + .build()); + + // Then + verify(emitter, times(2)).emit(captor.capture()); + + Map result = captor.getAllValues().get(0).getTrackerPayloads().get(0).getMap(); + assertEquals(ImmutableMap.builder() + .put("dtm", "123456") + .put("ttm", "456789") + .put("tz", "Etc/UTC") + .put("e", "pv") + .put("page", "title") + .put("tv", Version.TRACKER) + .put("p", "srv") + .put("eid", "9783090a-dace-4c85-a75c-933b4596a6c5") + .put("tna", "AF003") + .put("aid", "cloudfront") + .put("refr", "referer") + .put("url", "url") + .build(), result); + + Map result2 = captor.getAllValues().get(1).getTrackerPayloads().get(0).getMap(); + assertEquals(ImmutableMap.builder() + .put("dtm", "123456") + .put("ttm", "456789") + .put("tz", "Etc/UTC") + .put("e", "pv") + .put("page", "title") + .put("tv", Version.TRACKER) + .put("p", "srv") + .put("eid", "39139d43-ea13-4163-8559-adea258bf9c4") + .put("tna", "AF003") + .put("aid", "cloudfront") + .put("refr", "referer") + .put("url", "url") + .build(), result2); + } + @Test public void testTrackScreenView() { // When @@ -288,7 +343,7 @@ public void testTrackScreenView() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -317,7 +372,7 @@ public void testTrackScreenViewWithTimestamp() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -346,7 +401,7 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -378,7 +433,7 @@ public void testTrackTiming() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -416,7 +471,7 @@ public void testTrackTimingWithSubject() { // Then verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getMap(); + Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") @@ -447,8 +502,6 @@ public void testSetDefaultPlatform() throws Exception { .platform(DevicePlatform.Desktop) .build(); assertEquals(DevicePlatform.Desktop, tracker.getPlatform()); - tracker.setPlatform(DevicePlatform.ConnectedTV); - assertEquals(DevicePlatform.ConnectedTV, tracker.getPlatform()); } @Test @@ -473,23 +526,17 @@ public void testSetBase64Encoded() throws Exception { .base64(false) .build(); assertTrue(!tracker.getBase64Encoded()); - tracker.setBase64Encoded(true); - assertTrue(tracker.getBase64Encoded()); } @Test public void testSetAppId() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "AF003", "an-app-id").build(); assertEquals("an-app-id", tracker.getAppId()); - tracker.setAppId("cloudfront"); - assertEquals("cloudfront", tracker.getAppId()); } @Test public void testSetNamespace() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); assertEquals("namespace", tracker.getNamespace()); - tracker.setNamespace("cloudfront"); - assertEquals("cloudfront", tracker.getNamespace()); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 53c4e6cf..77b938d6 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -12,30 +12,31 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -// Java import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.UUID; -// Google import com.google.common.collect.Lists; -// JUnit import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -// Mockito import org.mockito.ArgumentCaptor; import static org.mockito.Mockito.*; -// This library +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import com.snowplowanalytics.snowplow.tracker.DevicePlatform; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; +import com.snowplowanalytics.snowplow.tracker.events.PageView; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; public class BatchEmitterTest { @@ -56,51 +57,75 @@ public void setUp() throws Exception { } @Test - @SuppressWarnings("AssertEqualsBetweenInconvertibleTypes") - public void addToBuffer_withLess10Payloads_shouldNotFlushBuffer() throws Exception { + public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws Exception { // Given ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(TrackerPayload.class); - List payloads = createPayloads(2); + List events = createEvents(2); // When - for (TrackerPayload payload : payloads) { - emitter.emit(payload); + for (TrackerEvent event : events) { + emitter.emit(event); } + Thread.sleep(500); + // Then - verify(emitter, never()).flushBuffer(); verify(httpClientAdapter, never()).get(argumentCaptor.capture()); Assert.assertEquals(2, emitter.getBuffer().size()); - Assert.assertEquals(payloads, emitter.getBuffer()); + Assert.assertEquals(events, emitter.getBuffer()); } @Test - @SuppressWarnings("AssertEqualsBetweenInconvertibleTypes") - public void addToBuffer_withMore10Payloads_shouldFlushBuffer() throws Exception { + public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Exception { // Given ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); - List payloads = createPayloads(10); + List events = createEvents(10); // When - for (TrackerPayload payload : payloads) { - emitter.emit(payload); + for (TrackerEvent event : events) { + emitter.emit(event); } Thread.sleep(500); // Then - verify(emitter).flushBuffer(); verify(httpClientAdapter).post(argumentCaptor.capture()); - List payloadMaps = new ArrayList<>(); - for (TrackerPayload payload : payloads) { - payloadMaps.add(payload.getMap()); + @SuppressWarnings("unchecked") + List> capturedPayload = (List>) argumentCaptor.getValue().getMap().get("data"); + + assertPayload(events, capturedPayload); + + Assert.assertEquals(0, emitter.getBuffer().size()); + } + + @Test + public void flushBuffer_shouldEmptyBuffer() throws Exception { + // Given + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); + + List events = createEvents(2); + + // When + for (TrackerEvent event : events) { + emitter.emit(event); } - Assert.assertEquals(payloadMaps, argumentCaptor.getValue().getMap().get("data")); - Assert.assertTrue(emitter.getBuffer().size() == 0); + emitter.flushBuffer(); + + Thread.sleep(500); + + // Then + verify(httpClientAdapter).post(argumentCaptor.capture()); + + @SuppressWarnings("unchecked") + List> capturedPayload = (List>) argumentCaptor.getValue().getMap().get(Parameter.DATA); + + assertPayload(events, capturedPayload); + + Assert.assertEquals(0, emitter.getBuffer().size()); } @Test @@ -110,15 +135,14 @@ public void setBufferSize_WithNegativeValue_ThrowInvalidArgumentException() thro } @Test - @SuppressWarnings("unchecked") public void getFinalPost_shouldAddSTMParameter() throws Exception { // Given ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); - List payloads = createPayloads(10); + List events = createEvents(10); // When - for (TrackerPayload payload : payloads) { - emitter.emit(payload); + for (TrackerEvent event : events) { + emitter.emit(event); } Thread.sleep(500); @@ -126,23 +150,54 @@ public void getFinalPost_shouldAddSTMParameter() throws Exception { // Then verify(httpClientAdapter).post(argumentCaptor.capture()); - ArrayList> dataList = (ArrayList>) argumentCaptor.getValue().getMap().get(Parameter.DATA); - for (Map payloadMap : dataList) { + @SuppressWarnings("unchecked") + List> capturedPayload = (List>) argumentCaptor.getValue().getMap().get(Parameter.DATA); + + for (Map payloadMap : capturedPayload) { Assert.assertTrue(payloadMap.containsKey(Parameter.DEVICE_SENT_TIMESTAMP)); } } - private List createPayloads(int nbPayload) { - final List payloads = Lists.newArrayList(); - for (int i = 0; i < nbPayload; i++) { - payloads.add(createPayload()); + private List createEvents(int numEvents) { + final List payloads = Lists.newArrayList(); + for (int i = 0; i < numEvents; i++) { + payloads.add(createEvent()); } return payloads; } - private TrackerPayload createPayload() { - TrackerPayload payload = new TrackerPayload(); - payload.add("id", UUID.randomUUID().toString()); - return payload; + private TrackerEvent createEvent() { + PageView pv = PageView.builder() + .pageUrl("https://www.snowplowanalytics.com/") + .pageTitle("Snowplow") + .referrer("https://www.google.com/") + .build(); + + return new TrackerEvent(pv, new TrackerParameters("appId", DevicePlatform.ServerSideApp, "namespace", "0.0.0", false), null); + } + + private void assertPayload(List events, List> capturedPayload) { + List> eventPayloads = new ArrayList<>(); + for (TrackerEvent event : events) { + //All PageView events so we can get(0) from payloads + eventPayloads.add(event.getTrackerPayloads().get(0).getMap()); + } + + //Iterate through all captured payloads + for (Map capturedMap : capturedPayload) { + boolean matchFound = false; + for (Map eventMap : eventPayloads) { + //Find the matching events + if (capturedMap.get("eid") == eventMap.get("eid")) { + matchFound = true; + + //Assert that all the entries in the event are in the captured payload + //There might be extra entries in capturedMap, such as the STM parameter + //check for these addtional parameters in other tests + assertThat(eventMap.entrySet(), everyItem(is(in(capturedMap.entrySet())))); + } + } + assertThat(matchFound, is(true)); //Ensure every event was found + } } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 80f1f520..9ba7f773 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -12,25 +12,20 @@ */ package com.snowplowanalytics.snowplow.tracker.http; -// Java import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.TimeUnit; -// Google import com.google.common.collect.ImmutableMap; -// SquareUp import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -// Apache import org.apache.http.impl.client.HttpClients; -// JUnit import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -38,7 +33,6 @@ import org.junit.runners.Parameterized; import static org.junit.Assert.assertEquals; -// This library import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; From f0a0325220f1ab58c63db3b908bd5df85d32d659 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Sun, 10 May 2020 10:13:51 +0100 Subject: [PATCH 031/128] Add default HttpClientAdapter so users do not have to create one (close #165) --- .../main/java/com/snowplowanalytics/Main.java | 24 +--------- .../tracker/emitter/AbstractEmitter.java | 33 +++++++++++-- .../emitter/BatchEmitterBuilderTest.java | 47 +++++++++++++++++++ 3 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index a345bba9..213d66e9 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -19,13 +19,9 @@ import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.emitter.RequestCallback; import com.snowplowanalytics.snowplow.tracker.events.*; -import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; -import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import okhttp3.OkHttpClient; - import java.util.List; import java.util.Set; import java.util.HashSet; @@ -43,30 +39,12 @@ public static String getUrlFromArgs(String[] args) { return args[0]; } - public static HttpClientAdapter getClient(String url) { - // use okhttp to send events - OkHttpClient client = new OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .build(); - - return OkHttpClientAdapter.builder() - .url(url) - .httpClient(client) - .build(); - } - public static void main(String[] args) { Set failedEventIds = new HashSet(); String collectorEndpoint = getUrlFromArgs(args); System.out.println("Sending events to " + collectorEndpoint); - // get the client adapter - // this is used by the Java tracker to transmit events to the collector - HttpClientAdapter okHttpClientAdapter = getClient(collectorEndpoint); - // the application id to attach to events String appId = "java-tracker-sample-console-app"; // the namespace to attach to events @@ -74,7 +52,7 @@ public static void main(String[] args) { // build an emitter, this is used by the tracker to batch and schedule transmission of events BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(okHttpClientAdapter) + .url(collectorEndpoint) .requestCallback(new RequestCallback() { // let us know on successes (may be called multiple times) @Override diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index 9e483f62..a0f84bed 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -15,12 +15,16 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; +import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import okhttp3.OkHttpClient; + /** * AbstractEmitter class which contains common elements to * the emitters wrapped in a builder format. @@ -33,9 +37,10 @@ public abstract class AbstractEmitter implements Emitter { public static abstract class Builder> { - private HttpClientAdapter httpClientAdapter; // Required + private HttpClientAdapter httpClientAdapter; // Optional private RequestCallback requestCallback = null; // Optional private int threadCount = 50; // Optional + private String collectorUrl = null; // Required if not specifying a httpClientAdapter protected abstract T self(); /** @@ -71,6 +76,18 @@ public T threadCount(final int threadCount) { this.threadCount = threadCount; return self(); } + + /** + * Sets the emitter url for when a httpClientAdapter is not specified + * Will be used to create the default OkHttpClientAdapter. + * + * @param collectorUrl the url for the default httpClientAdapter + * @return itself + */ + public T url(final String collectorUrl) { + this.collectorUrl = collectorUrl; + return self(); + } } private static class Builder2 extends Builder { @@ -87,10 +104,20 @@ public static Builder builder() { protected AbstractEmitter(final Builder builder) { // Precondition checks - Preconditions.checkNotNull(builder.httpClientAdapter); Preconditions.checkArgument(builder.threadCount > 0, "threadCount must be greater than 0"); - this.httpClientAdapter = builder.httpClientAdapter; + if (builder.httpClientAdapter != null) { + this.httpClientAdapter = builder.httpClientAdapter; + } else { + Preconditions.checkNotNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); + + this.httpClientAdapter = OkHttpClientAdapter.builder() + .url(builder.collectorUrl) + .httpClient( + new OkHttpClient()) // use okhttp as a default + .build(); + } + this.requestCallback = builder.requestCallback; this.executor = Executors.newScheduledThreadPool(builder.threadCount); } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java new file mode 100644 index 00000000..512eb504 --- /dev/null +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.mockito.Mockito.*; + +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; + +public class BatchEmitterBuilderTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void setNeitherHttpClientAdapterOrCollectorUrl_shouldThrowException() throws Exception { + expectedException.expect(Exception.class); + BatchEmitter.builder().build(); + } + + @Test + public void setCollectorUrlAndNoHttpClientAdapter_shouldInitialiseCorrectly() throws Exception { + BatchEmitter emitter = BatchEmitter.builder().url("https://mycollector.com").build(); + Assert.assertNotNull(emitter); + } + + @Test + public void setHttpClientAdapterAndNoCollectorUrl_shouldInitialiseCorrectly() throws Exception { + HttpClientAdapter httpClientAdapter = mock(HttpClientAdapter.class); + BatchEmitter emitter = BatchEmitter.builder().httpClientAdapter(httpClientAdapter).build(); + Assert.assertNotNull(emitter); + } +} From 1d150b286baacedc1cb6431959324c173e7e6e64 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:02:42 +0100 Subject: [PATCH 032/128] Upgrade to Gradle 6 (close #236) --- README.md | 4 + build.gradle | 109 +++++------------- examples/simple-console/README.md | 6 +- examples/simple-console/build.gradle | 27 +++-- .../gradle/wrapper/gradle-wrapper.properties | 3 +- examples/simple-console/gradlew | 28 ++++- examples/simple-console/gradlew.bat | 18 ++- gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55741 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 2 +- gradlew.bat | 2 +- 11 files changed, 96 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 86fd2c02..ab41c786 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ Assuming git, **[Vagrant][vagrant-install]** and **[VirtualBox][virtualbox-insta guest$ cd /vagrant guest$ ./gradlew clean build guest$ ./gradlew test +guest$ ./gradlew publishToMavenLocal +guest$ cd /examples/simple-console +guest$ ./gradlew jar +guest$ java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" ``` ## Find out more diff --git a/build.gradle b/build.gradle index d94f427a..ae15066a 100644 --- a/build.gradle +++ b/build.gradle @@ -11,27 +11,14 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ -buildscript { - repositories { - maven { url 'https://repo.spring.io/plugins-release' } - } - dependencies { - classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - } -} - plugins { - id "com.jfrog.bintray" version "1.8.4" + id 'com.jfrog.bintray' version '1.8.5' + id 'java-library' + id 'maven-publish' + id 'idea' } -apply plugin: 'java' -apply plugin: 'maven-publish' -apply plugin: 'idea' -apply plugin: 'propdeps' -apply plugin: 'propdeps-maven' -apply plugin: 'propdeps-idea' - -wrapper.gradleVersion = '5.0.0' +wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' @@ -54,44 +41,44 @@ configure([compileJava, compileTestJava]) { options.encoding = 'UTF-8' } -configurations { - provided -} - -sourceSets { - main { - compileClasspath += configurations.provided +java { + registerFeature('okhttpSupport') { + usingSourceSet(sourceSets.main) + } + registerFeature('apachehttpSupport') { + usingSourceSet(sourceSets.main) } } dependencies { // Apache Commons - compile 'commons-net:commons-net:3.3' + api 'commons-codec:commons-codec:1.10' + api 'commons-net:commons-net:3.3' // Apache HTTP - optional 'org.apache.httpcomponents:httpclient:4.3.3' - optional 'org.apache.httpcomponents:httpasyncclient:4.0.1' + apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.3.3' + apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.0.1' // Square OK HTTP - optional 'com.squareup.okhttp3:okhttp:4.2.2' + okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.2.2' // SLF4J logging API - compile 'org.slf4j:slf4j-api:1.7.7' - testRuntime 'org.slf4j:slf4j-simple:1.7.7' + api 'org.slf4j:slf4j-api:1.7.7' + testImplementation 'org.slf4j:slf4j-simple:1.7.7' // Jackson JSON processor - compile 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' // Preconditions - compile 'com.google.guava:guava:18.0' + api 'com.google.guava:guava:18.0' // Testing libraries - testCompile 'junit:junit:4.11' - testCompile 'org.hamcrest:hamcrest:2.2' - testCompile 'com.github.tomakehurst:wiremock:1.53' - testCompile 'org.skyscreamer:jsonassert:1.2.3' - testCompile 'org.mockito:mockito-core:3.2.4' - testCompile 'com.squareup.okhttp3:mockwebserver:4.2.1' + testImplementation 'junit:junit:4.11' + testImplementation 'org.hamcrest:hamcrest:2.2' + testImplementation 'com.github.tomakehurst:wiremock:1.53' + testImplementation 'org.skyscreamer:jsonassert:1.2.3' + testImplementation 'org.mockito:mockito-core:3.2.4' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.2.1' } task sourceJar(type: Jar, dependsOn: 'generateSources') { @@ -162,6 +149,10 @@ publishing { groupId group artifactId 'snowplow-java-tracker' version version + suppressPomMetadataWarningsFor('apachehttpSupportApiElements') + suppressPomMetadataWarningsFor('apachehttpSupportRuntimeElements') + suppressPomMetadataWarningsFor('okhttpSupportApiElements') + suppressPomMetadataWarningsFor('okhttpSupportRuntimeElements') pom { name = 'snowplow-java-tracker' description = 'Snowplow event tracker for Java. Add analytics to your Java desktop and server apps, servlets and games.' @@ -199,46 +190,6 @@ publishing { } } -install { - repositories.mavenInstaller { - pom.artifactId = 'snowplow-java-tracker' - pom.version = "$project.version" - pom.project { - name = 'snowplow-java-tracker' - description = 'Snowplow event tracker for Java. Add analytics to your Java desktop and server apps, servlets and games.' - url = 'https://github.com/snowplow/snowplow-java-tracker/' - inceptionYear = '2014' - - packaging = 'jar' - groupId = 'com.snowplowanalytics' - - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution = 'repo' - } - } - scm { - connection = 'https://github.com/snowplow/snowplow-java-tracker.git' - url = 'https://github.com/snowplow/snowplow-java-tracker' - } - developers { - developer { - name = 'Snowplow Analytics Ltd' - email = 'support@snowplowanalytics.com' - organization = 'Snowplow Analytics Ltd' - organizationUrl = 'http://snowplowanalytics.com' - } - } - organization { - name = 'com.snowplowanalytics' - url = 'http://snowplowanalytics.com' - } - } - } -} - bintray { user = System.getenv('BINTRAY_SNOWPLOW_MAVEN_USER') key = System.getenv('BINTRAY_SNOWPLOW_MAVEN_API_KEY') diff --git a/examples/simple-console/README.md b/examples/simple-console/README.md index 7dddded9..a089df68 100644 --- a/examples/simple-console/README.md +++ b/examples/simple-console/README.md @@ -1,10 +1,12 @@ # Simple console sample -This is a small Java console project that sends PageView events to a given collector. +This is a small Java console project that sends PageView events to a given collector. ## Run -``` +Ensure `build.gradle` is using a version available on the Snowplow maven repository, or a version installed to a local maven repository. + +```bash ./gradlew jar java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" ``` diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 31fbde6e..220ea07f 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -3,21 +3,24 @@ group = 'com.snowplowanalytics' version = '0.0.1' repositories { + mavenLocal() + maven { + url "https://snowplow.bintray.com/snowplow-maven" + } mavenCentral() - flatDir { - dirs '../../build/libs' - } } dependencies { - compile 'com.google.code.gson:gson:2.8+' - compile 'com.snowplowanalytics:snowplow-java-tracker:0.9.0' - compile 'com.squareup.okhttp3:okhttp:4.2.2' - compile 'org.slf4j:slf4j-api:1.7.7' - compile 'org.slf4j:slf4j-simple:1.7.7' - compile 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' - compile 'com.google.guava:guava:18.0' - testCompile 'junit:junit:4.12' + implementation 'com.snowplowanalytics:snowplow-java-tracker:0.9.0' + + implementation ('com.snowplowanalytics:snowplow-java-tracker:0.9.0') { + capabilities { + requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support:0.9.0' + } + } + + implementation 'org.slf4j:slf4j-simple:1.7.7' + testImplementation 'junit:junit:4.12' } task fatJar(type: Jar) { @@ -27,7 +30,7 @@ task fatJar(type: Jar) { 'Main-Class': 'com.snowplowanalytics.Main' } baseName = project.name + '-all' - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } } with jar } diff --git a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties index 8b6359a4..622ab64a 100644 --- a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties +++ b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Dec 05 10:27:05 GMT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-bin.zip diff --git a/examples/simple-console/gradlew b/examples/simple-console/gradlew index 4453ccea..83f2acfd 100755 --- a/examples/simple-console/gradlew +++ b/examples/simple-console/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,16 +44,16 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -155,7 +171,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/examples/simple-console/gradlew.bat b/examples/simple-console/gradlew.bat index e95643d6..24467a14 100644 --- a/examples/simple-console/gradlew.bat +++ b/examples/simple-console/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..457aad0d98108420a977756b7145c93c8910b076 100644 GIT binary patch delta 46476 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

d1ZzV`PJaf;LURr$w8USfDLW+{$cK=5Uq0( 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�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 V;3?$)Jo1M@amx&F&%^&5`+o%ZZcYFI diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 203de01d..622ab64a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Jan 09 17:33:41 EST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip diff --git a/gradlew b/gradlew index cccdd3d5..af6708ff 100755 --- a/gradlew +++ b/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index e95643d6..0f8d5937 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From 66d64bfc65d964f768e099abb8277dd697a59a77 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:36:33 +0100 Subject: [PATCH 033/128] Bump jackson-databind to 2.11.0 (close #235) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ae15066a..f166cf76 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ dependencies { testImplementation 'org.slf4j:slf4j-simple:1.7.7' // Jackson JSON processor - api 'com.fasterxml.jackson.core:jackson-databind:2.4.1.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.11.0' // Preconditions api 'com.google.guava:guava:18.0' From 232607d0469c3e0b0e2148dca4580e05bb28a6e1 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:45:06 +0100 Subject: [PATCH 034/128] Bump wiremock to 2.26.3 (close #237) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f166cf76..f2e58848 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { // Testing libraries testImplementation 'junit:junit:4.11' testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'com.github.tomakehurst:wiremock:1.53' + testImplementation 'com.github.tomakehurst:wiremock:2.26.3' testImplementation 'org.skyscreamer:jsonassert:1.2.3' testImplementation 'org.mockito:mockito-core:3.2.4' testImplementation 'com.squareup.okhttp3:mockwebserver:4.2.1' From d151fbea8d48ee72d432977d00f3c3bda5939b2c Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:46:46 +0100 Subject: [PATCH 035/128] Bump guava to 29.0 (close #238) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f2e58848..b6bc2f0c 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ dependencies { api 'com.fasterxml.jackson.core:jackson-databind:2.11.0' // Preconditions - api 'com.google.guava:guava:18.0' + api 'com.google.guava:guava:29.0-jre' // Testing libraries testImplementation 'junit:junit:4.11' From 04af6e900916c7e256b8f1bf9dd30498ac14958e Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:47:40 +0100 Subject: [PATCH 036/128] Bump mockwebserver to 4.7.2 (close #239) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b6bc2f0c..5904e2d2 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,7 @@ dependencies { testImplementation 'com.github.tomakehurst:wiremock:2.26.3' testImplementation 'org.skyscreamer:jsonassert:1.2.3' testImplementation 'org.mockito:mockito-core:3.2.4' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.2.1' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.7.2' } task sourceJar(type: Jar, dependsOn: 'generateSources') { From 94413fd26032ed4b223281c8362115569557ee0b Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:48:50 +0100 Subject: [PATCH 037/128] Bump commons-codec to 1.14 (close #241) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5904e2d2..a687243b 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ java { dependencies { // Apache Commons - api 'commons-codec:commons-codec:1.10' + api 'commons-codec:commons-codec:1.14' api 'commons-net:commons-net:3.3' // Apache HTTP From b13fd0dc74cf95adbbfd99be77e3d2cb226c13c5 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:49:30 +0100 Subject: [PATCH 038/128] Bump commons-net to 3.6 (close #245) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a687243b..7e3a9b8b 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ java { dependencies { // Apache Commons api 'commons-codec:commons-codec:1.14' - api 'commons-net:commons-net:3.3' + api 'commons-net:commons-net:3.6' // Apache HTTP apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.3.3' From 1ca739a5bffd91896b61281e784be416a6bf302c Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:51:56 +0100 Subject: [PATCH 039/128] Bump slf4j-api to 1.7.30 (close #246) --- build.gradle | 4 ++-- examples/simple-console/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 7e3a9b8b..ed15b775 100644 --- a/build.gradle +++ b/build.gradle @@ -63,8 +63,8 @@ dependencies { okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.2.2' // SLF4J logging API - api 'org.slf4j:slf4j-api:1.7.7' - testImplementation 'org.slf4j:slf4j-simple:1.7.7' + api 'org.slf4j:slf4j-api:1.7.30' + testImplementation 'org.slf4j:slf4j-simple:1.7.30' // Jackson JSON processor api 'com.fasterxml.jackson.core:jackson-databind:2.11.0' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 220ea07f..e5f9b7d1 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -19,7 +19,7 @@ dependencies { } } - implementation 'org.slf4j:slf4j-simple:1.7.7' + implementation 'org.slf4j:slf4j-simple:1.7.30' testImplementation 'junit:junit:4.12' } From 33cc7b6673648bd4824c5db7ac336136fe98442d Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:53:55 +0100 Subject: [PATCH 040/128] Bump mockito-core to 3.3.3 (close #247) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ed15b775..ef5e5410 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'com.github.tomakehurst:wiremock:2.26.3' testImplementation 'org.skyscreamer:jsonassert:1.2.3' - testImplementation 'org.mockito:mockito-core:3.2.4' + testImplementation 'org.mockito:mockito-core:3.3.3' testImplementation 'com.squareup.okhttp3:mockwebserver:4.7.2' } From 41d8010e05ed69fe1befd846478488c2ed5c5c63 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:55:49 +0100 Subject: [PATCH 041/128] Bump org.apache.httpcomponents:httpclient to 4.5.12 (close #248) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ef5e5410..c19085a9 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ dependencies { api 'commons-net:commons-net:3.6' // Apache HTTP - apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.3.3' + apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.12' apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.0.1' // Square OK HTTP From 8c82e601a853a7657604395e611318889c369f8c Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 16:56:59 +0100 Subject: [PATCH 042/128] Bump org.apache.httpcomponents:httpasyncclient to 4.1.4 (close #249) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c19085a9..9d1a4829 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ dependencies { // Apache HTTP apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.12' - apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.0.1' + apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.4' // Square OK HTTP okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.2.2' From b9dc172ec4dc727717bff8239cbe36e04aea0655 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Mon, 8 Jun 2020 17:20:30 +0100 Subject: [PATCH 043/128] Switch junit to native Gradle support (close #240) --- build.gradle | 12 ++++++++++-- examples/simple-console/build.gradle | 11 ++++++++++- .../test/java/com/snowplowanalytics/MainTest.java | 8 -------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 9d1a4829..7abe0e9d 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,12 @@ java { } } +test { + useJUnitPlatform { + includeEngines 'junit-vintage' + } +} + dependencies { // Apache Commons api 'commons-codec:commons-codec:1.14' @@ -73,10 +79,12 @@ dependencies { api 'com.google.guava:guava:29.0-jre' // Testing libraries - testImplementation 'junit:junit:4.11' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testCompileOnly 'junit:junit:4.13' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'com.github.tomakehurst:wiremock:2.26.3' - testImplementation 'org.skyscreamer:jsonassert:1.2.3' testImplementation 'org.mockito:mockito-core:3.3.3' testImplementation 'com.squareup.okhttp3:mockwebserver:4.7.2' } diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index e5f9b7d1..35632e70 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -10,6 +10,12 @@ repositories { mavenCentral() } +test { + useJUnitPlatform { + includeEngines 'junit-vintage' + } +} + dependencies { implementation 'com.snowplowanalytics:snowplow-java-tracker:0.9.0' @@ -20,7 +26,10 @@ dependencies { } implementation 'org.slf4j:slf4j-simple:1.7.30' - testImplementation 'junit:junit:4.12' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testCompileOnly 'junit:junit:4.13' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } task fatJar(type: Jar) { diff --git a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java index 2a0d8730..cf6e3011 100644 --- a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java +++ b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java @@ -37,12 +37,4 @@ public void testGetUrlEmpty() { Main.getUrlFromArgs(new String[]{}); } - - @Test - public void testGetClientAdapter() { - HttpClientAdapter givenClient = Main.getClient("https://acme.com"); - assertNotNull(givenClient); - assertEquals("https://acme.com", givenClient.getUrl()); - } - } \ No newline at end of file From c56d7cdf316f27f7ce2863512bae3ed557787aec Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Sun, 7 Jun 2020 12:08:18 +0100 Subject: [PATCH 044/128] Switch to GitHub Actions for build and release (close #231) --- .github/workflows/deploy.yml | 49 ++++++++++++++++++++++++++++++++++ .github/workflows/gradle.yml | 51 ++++++++++++++++++++++++++++++++++++ .travis.yml | 26 ------------------ .travis/deploy.sh | 14 ---------- 4 files changed, 100 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/gradle.yml delete mode 100644 .travis.yml delete mode 100755 .travis/deploy.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..28775cd9 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,49 @@ + +name: Deploy + +on: + push: + tags: + - '*.*.*' + +jobs: + deploy: + + runs-on: ubuntu-latest + + env: + BINTRAY_SNOWPLOW_MAVEN_USER: ${{ secrets.BINTRAY_SNOWPLOW_MAVEN_USER }} + BINTRAY_SNOWPLOW_MAVEN_API_KEY: ${{ secrets.BINTRAY_SNOWPLOW_MAVEN_API_KEY }} + SONA_USER: 'snowplow' + SONA_PASS: ${{ secrets.SONA_PASS }} + + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + + - name: Get tag and tracker version information + id: version + run: | + echo ::set-output name=TAG_VERSION::${GITHUB_REF#refs/*/} + echo "##[set-output name=TRACKER_VERSION;]$(./gradlew -q printVersion)" + + - name: Fail if version mismatch + if: ${{ steps.version.outputs.TAG_VERSION != steps.version.outputs.TRACKER_VERSION }} + run: | + echo "Tag version (${{ steps.version.outputs.TAG_VERSION }}) doesn't match version in project (${{ steps.version.outputs.TRACKER_VERSION }})" + exit 1 + + - name: Publish + run: ./gradlew bintrayUpload diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..a053a7d6 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,51 @@ + +name: Build + +on: [ push ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # build and test against LTS releases and latest GA + java: [ 8, 11, 13 ] + + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle Wrapper + run: ./gradlew build -x test + + - name: Test with Gradle Wrapper + run: ./gradlew test + + - name: Upload report if failed + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: Reports-JDK_${{ matrix.java }} + path: build/reports + + - uses: actions/upload-artifact@v1 + with: + name: Package-JDK_${{ matrix.java }} + path: build/libs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5a3f50fc..00000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -sudo: false -dist: trusty -language: java -jdk: -- openjdk8 -- oraclejdk8 -- openjdk11 -- oraclejdk11 -script: -- "./gradlew build" -deploy: - skip_cleanup: true - provider: script - script: "./.travis/deploy.sh $TRAVIS_TAG" - on: - condition: '"${TRAVIS_JDK_VERSION}" == "openjdk8"' - tags: true -env: - global: - # BINTRAY_SNOWPLOW_MAVEN_USER - - secure: Sk7Xf0TEXyDKtZxICiDVZkDEnDkSSe3m2+j1FWhLNEVfVDGqY9j4mo84S9qNOGjblJ6LbLa91NPhGFaNa1E0WAb9Zlf7e82nELTufGmoOn006Tw/nSEy8Vpvbjh+OZ+wweGYSghWYvjYKmUtlwpwBDyHezblVmpBa9tLg/2Ajzw= - # BINTRAY_SNOWPLOW_MAVEN_API_KEY - - secure: aqeXPiW/VAZNQJiE9z2S8Z/bshyVXiepczXIDlyesKe//qNaQ3X6A5Ozh8r0KCk1TCJESW5fFzBzD1kla/aDK7clW9GFQ3U29aWgXcGcLDu4plslKK+sGt/yDhMVEpD1qjLhI9mIwj6enDCvIlEtjVnrVkqaN2pjXreemE+F2UU= - - SONA_USER=snowplow - # SONA_PASS - - secure: QjjbsUJXsD/jiWXW/5vKm6obp/0SASVRxFVtVLUCee4euPTd5faCXP0gdr1IbnNW7iLbYlk+FExw2N9CzWpfjr1EWz+U405znkR6YCMMWIQ0WKgzGzEgy/19vQhPI3SPy4ymiDEh7tbDmvmMdnmtX2+btRAGWcPp2oUSlbSldCk= diff --git a/.travis/deploy.sh b/.travis/deploy.sh deleted file mode 100755 index ef8d12b4..00000000 --- a/.travis/deploy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -tag_version=$1 - -cd $TRAVIS_BUILD_DIR -pwd - -project_version=`./gradlew -q printVersion` -if [ "${project_version}" == "${tag_version}" ]; then - ./gradlew bintrayUpload -else - echo "Tag version '${tag_version}' doesn't match version in project ('${project_version}'). Aborting!" - exit 1 -fi From 19824e74153bd6f153e040949ee62621f8678816 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Tue, 9 Jun 2020 22:29:02 +0100 Subject: [PATCH 045/128] Update README to point to docs.snowplowanalytics.com (close #251) --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ab41c786..7e9b2310 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Java Analytics for Snowplow -[![Build Status][travis-image]][travis] [![Release][release-image]][releases] [![License][license-image]][license] +[![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] ## Overview @@ -34,7 +34,7 @@ guest$ java -jar ./build/libs/simple-console-all-0.0.1.jar "http:// Date: Tue, 9 Jun 2020 22:34:26 +0100 Subject: [PATCH 046/128] Update copyright years (close #227) --- build.gradle | 4 ++-- .../src/main/java/com/snowplowanalytics/Main.java | 2 +- .../src/test/java/com/snowplowanalytics/MainTest.java | 2 +- .../snowplowanalytics/snowplow/tracker/DevicePlatform.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Subject.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Tracker.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Utils.java | 2 +- .../snowplow/tracker/constants/Constants.java | 2 +- .../snowplow/tracker/constants/Parameter.java | 2 +- .../snowplow/tracker/emitter/AbstractEmitter.java | 2 +- .../snowplow/tracker/emitter/BatchEmitter.java | 2 +- .../snowplowanalytics/snowplow/tracker/emitter/Emitter.java | 2 +- .../snowplow/tracker/emitter/RequestCallback.java | 2 +- .../snowplow/tracker/emitter/SimpleEmitter.java | 2 +- .../snowplow/tracker/events/AbstractEvent.java | 2 +- .../snowplow/tracker/events/EcommerceTransaction.java | 2 +- .../snowplow/tracker/events/EcommerceTransactionItem.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Event.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/PageView.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/ScreenView.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/Structured.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Timing.java | 2 +- .../snowplow/tracker/events/Unstructured.java | 2 +- .../snowplow/tracker/http/AbstractHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/ApacheHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/HttpClientAdapter.java | 2 +- .../snowplow/tracker/http/OkHttpClientAdapter.java | 2 +- .../snowplowanalytics/snowplow/tracker/payload/Payload.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJson.java | 2 +- .../snowplow/tracker/payload/TrackerEvent.java | 2 +- .../snowplow/tracker/payload/TrackerParameters.java | 2 +- .../snowplow/tracker/payload/TrackerPayload.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/SubjectTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/UtilsTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterBuilderTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterTest.java | 2 +- .../snowplow/tracker/http/HttpClientAdapterTest.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJsonTest.java | 2 +- .../snowplow/tracker/payload/TrackerPayloadTest.java | 2 +- 40 files changed, 41 insertions(+), 41 deletions(-) diff --git a/build.gradle b/build.gradle index 7abe0e9d..83008fee 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -101,7 +101,7 @@ task generateSources { srcFile.parentFile.mkdirs() srcFile.write( """/* - * Copyright (c) 2014-2019 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 213d66e9..9e65aaa3 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java index cf6e3011..23341b8b 100644 --- a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java +++ b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java index ca62565d..c04d8f80 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index f591c93c..5f9d0e64 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 0f8cdf0e..a2390ede 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index ebcfdf3d..8102814a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java index 89c90c21..5d3e0b4f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index 16ce945d..1202985a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index a0f84bed..e200c140 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index c5024f9a..f7e9baa2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index e1cd535f..e49aa8d2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java index deb9cf05..217b599b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 19c39c18..f78274aa 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 8337541f..6c366585 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index e87f258a..12ef9b63 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index 33c9b789..4d2eb201 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 0c40230b..192943d5 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index d167a541..11c12c0a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 034bd395..4d56ce88 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index a0a98ee3..c1b91c33 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index 696b7c37..2ea85904 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index 5a5f34cd..acc76b53 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index ebd3c96c..959891e7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 1499d294..d326917d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java index f858b62c..f462ceea 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 34f17a82..9906e8b0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java index 8413a0b8..312f1ced 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index e7f88c85..2be2b640 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java index d6ef6eef..10f1661a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java index f94abbf8..5c0264e0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 3a81ff1c..61b8efa3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index ba667bfd..3a293c9d 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 713aef74..5f01f736 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index f1074e3f..e98fec71 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java index 512eb504..af479c9a 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 77b938d6..302ea57d 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 9ba7f773..ac1f3790 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java index b7497160..da3151f2 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java index 144b406b..38235d6e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. From d3e6c3feb9b8a26b5bf46921a48393918e2ab27a Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Wed, 10 Jun 2020 09:09:42 +0100 Subject: [PATCH 047/128] Add snyk monitor to Github Actions (close #253) --- .github/workflows/snyk.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/snyk.yml diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 00000000..51c8f4fc --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,21 @@ + +name: Snyk + +on: + push: + branches: [ master ] + +jobs: + security: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/gradle@master + with: + command: monitor + args: --org=data-value + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} From f07d7f5aaf34fa6e14711537f5b239bc063856e1 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Tue, 9 Jun 2020 22:13:47 +0100 Subject: [PATCH 048/128] Prepare for release --- CHANGELOG | 46 ++++++++++++++----- build.gradle | 2 +- examples/simple-console/build.gradle | 6 +-- .../snowplow/tracker/TrackerTest.java | 2 +- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 103901a2..42414d2e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,19 +1,43 @@ +Java 0.10.0 (2020-06-10) +----------------------- +Add snyk monitor to Github Actions (#253) +Update copyright years (#227) +Update README to point to docs.snowplowanalytics.com (#251) +Switch to GitHub Actions for build and release (#231) +Switch junit to native Gradle support (#240) +Bump org.apache.httpcomponents:httpasyncclient to 4.1.4 (#249) +Bump org.apache.httpcomponents:httpclient to 4.5.12 (#248) +Bump mockito-core to 3.3.3 (#247) +Bump slf4j-api to 1.7.30 (#246) +Bump commons-net to 3.6 (#245) +Bump commons-codec to 1.14 (#241) +Bump mockwebserver to 4.7.2 (#239) +Bump guava to 29.0 (#238) +Bump wiremock to 2.26.3 (#237) +Bump jackson-databind to 2.11.0 (#235) +Upgrade to Gradle 6 (#236) +Add default HttpClientAdapter so users do not have to create one (#165) +Support for creating TrackerPayload asynchronously (#222) +Add POM information to Maven Publishing section in build.gradle (#234) +Remove use of deprecated OkHttp methods (#228) +Switch build.gradle to use https://repo.spring.io/plugins-release (#223) + Java 0.9.0 (2019-12-24) ----------------------- -Bump OkHttp to OkHttp3 version 4 (close #175) -Add STM to outbound events (close #169) -Add support for attaching true timestamp to events (close #178) -Update all non-static Loggers to static (close #213) -Fix events sent by example simple-console (close #221) -Alter logging for invalid keys only when adding to TrackerPayload (close #186) -Fix Peru version so vagrant up succeeds (close #216) -Fix Javadoc generation warnings (close #219) +Bump OkHttp to OkHttp3 version 4 (#175) +Add STM to outbound events (#169) +Add support for attaching true timestamp to events (#178) +Update all non-static Loggers to static (#213) +Fix events sent by example simple-console (#221) +Alter logging for invalid keys only when adding to TrackerPayload (#186) +Fix Peru version so vagrant up succeeds (#216) +Fix Javadoc generation warnings (#219) Java 0.8.4 (2019-01-09) ----------------------- -Add deployment to build process (close #183) -Add sonatype credentials to .travis.yml (closes #209) -Add Bintray credentials to .travis.yml (closes #208) +Add deployment to build process (#183) +Add sonatype credentials to .travis.yml (#209) +Add Bintray credentials to .travis.yml (#208) Java 0.8.3 (2019-01-02) ----------------------- diff --git a/build.gradle b/build.gradle index 83008fee..8ab4bcbd 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.9.0' +version = '0.10.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 35632e70..63dd1fa7 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -17,11 +17,11 @@ test { } dependencies { - implementation 'com.snowplowanalytics:snowplow-java-tracker:0.9.0' + implementation 'com.snowplowanalytics:snowplow-java-tracker:0.10.0' - implementation ('com.snowplowanalytics:snowplow-java-tracker:0.9.0') { + implementation ('com.snowplowanalytics:snowplow-java-tracker:0.10.0') { capabilities { - requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support:0.9.0' + requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support:0.10.0' } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 5f01f736..76e56bff 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -493,7 +493,7 @@ public void testTrackTimingWithSubject() { @Test public void testGetTrackerVersion() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); - assertEquals("java-0.9.0", tracker.getTrackerVersion()); + assertEquals("java-0.10.0", tracker.getTrackerVersion()); } @Test From 8261bca9b22267ea3a6b8efed4faa8556d9399c5 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Thu, 11 Jun 2020 11:58:05 +0100 Subject: [PATCH 049/128] Update snyk integration to include project name in GitHub action (close #256) --- .github/workflows/snyk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 51c8f4fc..a26496d3 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -16,6 +16,6 @@ jobs: uses: snyk/actions/gradle@master with: command: monitor - args: --org=data-value + args: --project-name=snowplow-java-tracker env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} From 832b2d05193e81575e629c1f4118c9bbd255d2ba Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Thu, 11 Jun 2020 12:13:24 +0100 Subject: [PATCH 050/128] Publish Gradle module file with bintrayUpload (close #255) --- build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build.gradle b/build.gradle index 8ab4bcbd..3225597f 100644 --- a/build.gradle +++ b/build.gradle @@ -198,6 +198,18 @@ publishing { } } +// Workaround for upload of module.json file, remove when issue fixed https://github.com/bintray/gradle-bintray-plugin/issues/229 +bintrayUpload.doFirst { + publishing.publications.all { publication -> + def moduleFile = file("$buildDir/publications/$publication.name/module.json") + if (moduleFile.exists()) { + artifact(moduleFile) { + extension = "module" + } + } + } +} + bintray { user = System.getenv('BINTRAY_SNOWPLOW_MAVEN_USER') key = System.getenv('BINTRAY_SNOWPLOW_MAVEN_API_KEY') From 65d3a0cca839ed6dbc111afcaf4bac639fd41c25 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Thu, 11 Jun 2020 12:16:32 +0100 Subject: [PATCH 051/128] Prepare for release --- CHANGELOG | 5 +++++ build.gradle | 2 +- examples/simple-console/build.gradle | 6 +++--- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 42414d2e..84d079bd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +Java 0.10.1 (2020-06-11) +----------------------- +Publish Gradle module file with bintrayUpload (#255) +Update snyk integration to include project name in GitHub action (#256) + Java 0.10.0 (2020-06-10) ----------------------- Add snyk monitor to Github Actions (#253) diff --git a/build.gradle b/build.gradle index 3225597f..8d39d065 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.10.0' +version = '0.10.1' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 63dd1fa7..a722fa83 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -17,11 +17,11 @@ test { } dependencies { - implementation 'com.snowplowanalytics:snowplow-java-tracker:0.10.0' + implementation 'com.snowplowanalytics:snowplow-java-tracker:0.10.1' - implementation ('com.snowplowanalytics:snowplow-java-tracker:0.10.0') { + implementation ('com.snowplowanalytics:snowplow-java-tracker:0.10.1') { capabilities { - requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support:0.10.0' + requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support:0.10.1' } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 76e56bff..9fe78f48 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -493,7 +493,7 @@ public void testTrackTimingWithSubject() { @Test public void testGetTrackerVersion() throws Exception { Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); - assertEquals("java-0.10.0", tracker.getTrackerVersion()); + assertEquals("java-0.10.1", tracker.getTrackerVersion()); } @Test From 09caf007e781aaf9dadd1738dc013f9988fdc14e Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Fri, 10 Jul 2020 16:20:58 +0100 Subject: [PATCH 052/128] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 27 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..8b8914db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior or code snippets that produce the issue. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + - OS: [e.g. Ubuntu 20.04] + - Version [e.g. 3.8] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From d1cde4fa5de76c60604560586efd94192a24073b Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Fri, 10 Jul 2020 16:22:52 +0100 Subject: [PATCH 053/128] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8b8914db..ccaa3718 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,7 +21,7 @@ If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - OS: [e.g. Ubuntu 20.04] - - Version [e.g. 3.8] + - Version [e.g. Java 12] **Additional context** Add any other context about the problem here. From 8e785ad7ae95f0e7148406224fb4abc1f9218682 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Tue, 14 Jul 2020 12:15:43 +0100 Subject: [PATCH 054/128] Add CONTRIBUTING.md (closes #260) --- CONTRIBUTING.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1decb69b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing + +The Snowplow Java Tracker is maintained by the Engineering team at Snowplow Analytics. We welcome suggestions for improvements and bug fixes to all Snowplow Trackers. + +We are extremely grateful for all contributions we receive, whether that is reporting an issue or a change to the code which can be made in the form of a pull request. + +For support requests, please use our community support Discourse forum: https://discourse.snowplowanalytics.com/. + +## Setting up an Environment + +Instructions on how to build and run tests are available in the [README.md](README.md). The README will also list any requirements that you will need to install first before being able to build and run the tests. + +You should ensure you are comfortable building and testing the existing release before adding new functionality or fixing issues. + +## Issues + +### Creating an issue + +The project contains an issue template which should help guiding you through the process. However, please keep in mind that support requests should go to our Discourse forum: https://discourse.snowplowanalytics.com/ and not GitHub issues. + +It's also a good idea to log an issue before starting to work on a pull request to discuss it with the maintainers. A pull request is just one solution to a problem and it is often a good idea to talk about the problem with the maintainers first. + +### Working on an issue + +If you see an issue you would like to work on, please let us know in the issue! That will help us in terms of scheduling and +not doubling the amount of work. + +If you don't know where to start contributing, you can look at +[the issues labeled `good first issue`](https://github.com/snowplow/snowplow-java-tracker/labels/good%20first%20issue). + +## Pull requests + +These are a few guidelines to keep in mind when opening pull requests. + +### Guidelines + +Please supply a good PR description. These are very helpful and help the maintainers to understand _why_ the change has been made, not just _what_ changes have been made. + +Please try and keep your PR to a single feature of fix. This might mean breaking up a feature into multiple PRs but this makes it easier for the maintainers to review and also reduces the risk in each change. + +Please review your own PR as you would do it you were a reviewer first. This is a great way to spot any mistakes you made when writing the change. Additionally, ensure your code compiles and all tests pass. + +### Commit hygiene + +We keep a strict 1-to-1 correspondance between commits and issues, as such our commit messages are formatted in the following +fashion: + +`Issue Description (closes #1234)` + +for example: + +`Fix Issue with Tracker (closes #1234)` + +### Writing tests + +Whenever necessary, it's good practice to add the corresponding tests to whichever feature you are working on. +Any non-trivial PR must have tests and will not be accepted without them. + +### Feedback cycle + +Reviews should happen fairly quickly during weekdays. +If you feel your pull request has been forgotten, please ping one or more maintainers in the pull request. + +### Getting your pull request merged + +If your pull request is fairly chunky, there might be a non-trivial delay between the moment the pull request is approved and the moment it gets merged. This is because your pull request will have been scheduled for a specific milestone which might or might not be actively worked on by a maintainer at the moment. + +### Contributor license agreement + +We require outside contributors to sign a Contributor license agreement (or CLA) before we can merge their pull requests. +You can find more information on the topic in [the dedicated wiki page](https://github.com/snowplow/snowplow/wiki/CLA). +The @snowplowcla bot will guide you through the process. + +## Getting in touch + +### Community support requests + +Please do not log an issue if you are asking for support, all of our community support requests go through our Discourse forum: https://discourse.snowplowanalytics.com/. + +Posting your problem there ensures more people will see it and you should get support faster than creating a new issue on GitHub. Please do create a new issue on GitHub if you think you've found a bug though! \ No newline at end of file From 73cd5a496212fd38771c3f0d64a9ecba92f490e2 Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Tue, 14 Jul 2020 12:15:58 +0100 Subject: [PATCH 055/128] Add Snowplow Maintenance Badge (closes #261) --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e9b2310..6c4d52ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Java Analytics for Snowplow -[![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] +[![early-release]][tracker-classificiation] [![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] ## Overview @@ -30,7 +30,7 @@ guest$ java -jar ./build/libs/simple-console-all-0.0.1.jar "http:// Date: Wed, 17 Nov 2021 11:45:24 +0000 Subject: [PATCH 056/128] Replace Vagrant with Docker (close #267) --- .dockerignore | 9 + .gitignore | 10 +- Dockerfile | 4 + README.md | 60 ++-- Vagrantfile | 24 -- build.gradle | 2 +- examples/simple-console/build.gradle | 9 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/simple-console/gradlew | 260 +++++++++++------- examples/simple-console/gradlew.bat | 25 +- gradle/wrapper/gradle-wrapper.jar | Bin 55741 -> 58910 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 53 ++-- gradlew.bat | 22 +- vagrant/.gitignore | 8 - vagrant/ansible.hosts | 2 - vagrant/peru.yaml | 14 - vagrant/push.bash | 4 - vagrant/up.bash | 50 ---- vagrant/up.guidance | 4 - vagrant/up.playbooks | 2 - 22 files changed, 279 insertions(+), 287 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 Vagrantfile create mode 100644 examples/simple-console/gradle/wrapper/gradle-wrapper.jar delete mode 100644 vagrant/.gitignore delete mode 100644 vagrant/ansible.hosts delete mode 100644 vagrant/peru.yaml delete mode 100755 vagrant/push.bash delete mode 100755 vagrant/up.bash delete mode 100644 vagrant/up.guidance delete mode 100644 vagrant/up.playbooks diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3d294d96 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +.gitignore + +.idea +.DS_Store + +build/ +examples/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6887f746..ee9599a2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ *.war *.ear +# Don't ignore gradle wrapper +!gradle-wrapper.jar + # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* @@ -54,8 +57,9 @@ local.properties # Ignoring Version.java since its auto-generated Version.java -# Vagrant -.vagrant - #macOS .DS_Store + +# Eclipse +.project +.settings/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..cca23c3c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:11 +COPY . /java-tracker +WORKDIR /java-tracker +RUN ./gradlew build diff --git a/README.md b/README.md index 6c4d52ce..1a4ba234 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,47 @@ # Java Analytics for Snowplow -[![early-release]][tracker-classificiation] [![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] +[![early-release]][tracker-classification] [![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] ## Overview Add analytics to your Java software with the **[Snowplow][snowplow]** event tracker for **[Java][java]**. See also: **[Snowplow Android Tracker][snowplow-android-tracker]**. -With this tracker you can collect event data from your Java-based desktop and server apps, servlets and games. Supports JDK7+. +With this tracker you can collect event data from your Java-based desktop and server apps, servlets and games. Supports JDK8+. -## Quickstart +## Find out more + +| Snowplow Docs | Contributing | +|---------------------------------|-----------------------------------| +| ![i1][techdocs-image] | ![i4][contributing-image] | +| **[Snowplow Docs][techdocs]** | **[Contributing](CONTRIBUTING.md)** | + +## Maintainer Quickstart + +Feedback and contributions are very welcome. If you have identified a bug, please log an issue on this repo. For all other feedback, discussion or questions please open a thread on our [Discourse forum][forums]. Feel free to make Pull Requests for new features, if you can code them yourself! -Assuming git, **[Vagrant][vagrant-install]** and **[VirtualBox][virtualbox-install]** installed: +Clone this repo and navigate into the cloned folder. To run the tests locally, you will need Docker or Java installed. Using either method, the build will fail if there are failing tests. + +To run the tests using Docker, run: ```bash - host$ git clone https://github.com/snowplow/snowplow-java-tracker.git - host$ cd snowplow-java-tracker - host$ vagrant up && vagrant ssh -guest$ cd /vagrant -guest$ ./gradlew clean build -guest$ ./gradlew test -guest$ ./gradlew publishToMavenLocal -guest$ cd /examples/simple-console -guest$ ./gradlew jar -guest$ java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" +$ docker build . -t snowplow-java-tracker ``` -## Find out more +To run the tests using your installed JDK, run: -| Technical Docs | Setup Guide | Roadmap | Contributing | -|---------------------------------|---------------------------|-------------------------|-----------------------------------| -| ![i1][techdocs-image] | ![i2][setup-image] | ![i3][roadmap-image] | ![i4][contributing-image] | -| **[Technical Docs][techdocs]** | **[Setup Guide][setup]** | **[Roadmap][roadmap]** | **[Contributing](Contributing.md)** | +```bash +$ ./gradlew build +``` + +We have also included a simple demo, found in the `examples/simple-console` folder. You will need a JDK installed to run it. When run, it sends several events to your event collector. + +```bash +$ ./gradlew publishToMavenLocal +$ cd examples/simple-console +$ ./gradlew jar +$ java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" +``` +For a simple event collector, we advise using the [Snowplow Micro][micro] testing pipeline. ## Copyright and license @@ -57,19 +68,16 @@ limitations under the License. [java]: http://www.java.com/en/ [snowplow]: http://snowplowanalytics.com +[forums]: https://discourse.snowplowanalytics.com/ [snowplow-android-tracker]: https://github.com/snowplow/snowplow-android-tracker/ - -[vagrant-install]: http://docs.vagrantup.com/v2/installation/index.html -[virtualbox-install]: https://www.virtualbox.org/wiki/Downloads +[micro]: https://github.com/snowplow-incubator/snowplow-micro [techdocs-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/techdocs.png [setup-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/setup.png [roadmap-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/roadmap.png [contributing-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/contributing.png -[techdocs]: https://github.com/snowplow/snowplow/wiki/Java-Tracker -[setup]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/java-tracker/setup/ -[roadmap]: https://github.com/snowplow/snowplow/wiki/Java-Tracker-Roadmap +[techdocs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/java-tracker/ -[tracker-classificiation]: https://github.com/snowplow/snowplow/wiki/Tracker-Maintenance-Classification +[tracker-classification]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/tracker-maintenance-classification/ [early-release]: https://img.shields.io/static/v1?style=flat&label=Snowplow&message=Early%20Release&color=014477&labelColor=9ba0aa&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEVMaXGXANeYANeXANZbAJmXANeUANSQAM+XANeMAMpaAJhZAJeZANiXANaXANaOAM2WANVnAKWXANZ9ALtmAKVaAJmXANZaAJlXAJZdAJxaAJlZAJdbAJlbAJmQAM+UANKZANhhAJ+EAL+BAL9oAKZnAKVjAKF1ALNBd8J1AAAAKHRSTlMAa1hWXyteBTQJIEwRgUh2JjJon21wcBgNfmc+JlOBQjwezWF2l5dXzkW3/wAAAHpJREFUeNokhQOCA1EAxTL85hi7dXv/E5YPCYBq5DeN4pcqV1XbtW/xTVMIMAZE0cBHEaZhBmIQwCFofeprPUHqjmD/+7peztd62dWQRkvrQayXkn01f/gWp2CrxfjY7rcZ5V7DEMDQgmEozFpZqLUYDsNwOqbnMLwPAJEwCopZxKttAAAAAElFTkSuQmCC diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 7cf18f20..00000000 --- a/Vagrantfile +++ /dev/null @@ -1,24 +0,0 @@ -Vagrant.configure("2") do |config| - - config.vm.box = "ubuntu/trusty64" - config.vm.hostname = "snowplow-java-tracker" - config.ssh.forward_agent = true - - config.vm.provider :virtualbox do |vb| - vb.name = Dir.pwd().split("/")[-1] + "-" + Time.now.to_f.to_i.to_s - vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] - vb.customize [ "guestproperty", "set", :id, "--timesync-threshold", 10000 ] - # Need a bit of memory for Java - vb.memory = 2560 - end - - config.vm.provision :shell do |sh| - sh.path = "vagrant/up.bash" - end - - # Requires Vagrant 1.7.0+ - config.push.define "binary", strategy: "local-exec" do |push| - push.script = "vagrant/push.bash" - end - -end diff --git a/build.gradle b/build.gradle index 8d39d065..5aaf39af 100644 --- a/build.gradle +++ b/build.gradle @@ -172,7 +172,7 @@ publishing { licenses { license { - name = 'The Apache Software License, Version 2.0' + name = 'Apache License, Version 2.0' url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' distribution = 'repo' } diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index a722fa83..0497ec92 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'java' group = 'com.snowplowanalytics' version = '0.0.1' +sourceCompatibility = '1.8' +targetCompatibility = '1.8' repositories { mavenLocal() @@ -39,8 +41,13 @@ task fatJar(type: Jar) { 'Main-Class': 'com.snowplowanalytics.Main' } baseName = project.name + '-all' - from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + from { + configurations.compileClasspath.collect { + it.isDirectory() ? it : zipTree(it) + } + } with jar + duplicatesStrategy DuplicatesStrategy.EXCLUDE } tasks.jar.dependsOn(fatJar) diff --git a/examples/simple-console/gradle/wrapper/gradle-wrapper.jar b/examples/simple-console/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties index 622ab64a..ffed3a25 100644 --- a/examples/simple-console/gradle/wrapper/gradle-wrapper.properties +++ b/examples/simple-console/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/simple-console/gradlew b/examples/simple-console/gradlew index 83f2acfd..1b6c7873 100755 --- a/examples/simple-console/gradlew +++ b/examples/simple-console/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/examples/simple-console/gradlew.bat b/examples/simple-console/gradlew.bat index 24467a14..ac1b06f9 100644 --- a/examples/simple-console/gradlew.bat +++ b/examples/simple-console/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 457aad0d98108420a977756b7145c93c8910b076..62d4c053550b91381bbd28b1afc82d634bf73a8a 100644 GIT binary patch delta 32376 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 delta 29406 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 VVIhveA#DWt=s`w1slLa5{}0SD(6j&m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 622ab64a..ffed3a25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708ff..fbd7c515 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 0f8d5937..a9f778a7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -65,6 +84,7 @@ set CMD_LINE_ARGS=%* set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% diff --git a/vagrant/.gitignore b/vagrant/.gitignore deleted file mode 100644 index d0e94a5b..00000000 --- a/vagrant/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -* -!.gitignore -!peru.yaml -!ansible.hosts -!up.bash -!up.playbooks -!up.guidance -!push.bash diff --git a/vagrant/ansible.hosts b/vagrant/ansible.hosts deleted file mode 100644 index 588fa08c..00000000 --- a/vagrant/ansible.hosts +++ /dev/null @@ -1,2 +0,0 @@ -[vagrant] -127.0.0.1:2222 diff --git a/vagrant/peru.yaml b/vagrant/peru.yaml deleted file mode 100644 index e7fdf41c..00000000 --- a/vagrant/peru.yaml +++ /dev/null @@ -1,14 +0,0 @@ -imports: - ansible: ansible - ansible_playbooks: oss-playbooks - -curl module ansible: - # Equivalent of git cloning tags/v1.6.6 but much, much faster - url: https://codeload.github.com/ansible/ansible/zip/69d85c22c7475ccf8169b6ec9dee3ee28c92a314 - unpack: zip - export: ansible-69d85c22c7475ccf8169b6ec9dee3ee28c92a314 - -git module ansible_playbooks: - url: https://github.com/snowplow/ansible-playbooks.git - # Comment out to fetch a specific rev instead of master: - # rev: xxx diff --git a/vagrant/push.bash b/vagrant/push.bash deleted file mode 100755 index df2ec16a..00000000 --- a/vagrant/push.bash +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -e - -echo "Not yet implemented, see https://github.com/snowplow/snowplow-java-tracker/issues/107" diff --git a/vagrant/up.bash b/vagrant/up.bash deleted file mode 100755 index 8613cd82..00000000 --- a/vagrant/up.bash +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -e - -vagrant_dir=/vagrant/vagrant -bashrc=/home/vagrant/.bashrc - -echo "========================================" -echo "INSTALLING PERU AND ANSIBLE DEPENDENCIES" -echo "----------------------------------------" -apt-get update -apt-get install -y language-pack-en git unzip libyaml-dev python3-pip python-yaml python-paramiko python-jinja2 - -echo "===============" -echo "INSTALLING PERU" -echo "---------------" -sudo pip3 install peru==1.1.4 - -echo "=======================================" -echo "CLONING ANSIBLE AND PLAYBOOKS WITH PERU" -echo "---------------------------------------" -cd ${vagrant_dir} && peru sync -v -echo "... done" - -env_setup=${vagrant_dir}/ansible/hacking/env-setup -hosts=${vagrant_dir}/ansible.hosts - -echo "===================" -echo "CONFIGURING ANSIBLE" -echo "-------------------" -touch ${bashrc} -echo "source ${env_setup}" >> ${bashrc} -echo "export ANSIBLE_HOSTS=${hosts}" >> ${bashrc} -echo "... done" - -echo "==========================================" -echo "RUNNING PLAYBOOKS WITH ANSIBLE*" -echo "* no output while each playbook is running" -echo "------------------------------------------" -while read pb; do - su - -c "source ${env_setup} && ${vagrant_dir}/ansible/bin/ansible-playbook ${vagrant_dir}/${pb} --connection=local --inventory-file=${hosts}" vagrant -done <${vagrant_dir}/up.playbooks - -guidance=${vagrant_dir}/up.guidance - -if [ -f ${guidance} ]; then - echo "===========" - echo "PLEASE READ" - echo "-----------" - cat $guidance -fi diff --git a/vagrant/up.guidance b/vagrant/up.guidance deleted file mode 100644 index c359beac..00000000 --- a/vagrant/up.guidance +++ /dev/null @@ -1,4 +0,0 @@ -To get started: -vagrant ssh -cd /vagrant -gradle test diff --git a/vagrant/up.playbooks b/vagrant/up.playbooks deleted file mode 100644 index 9acf93fd..00000000 --- a/vagrant/up.playbooks +++ /dev/null @@ -1,2 +0,0 @@ -oss-playbooks/java7.yml -oss-playbooks/gradle.yml From b4569c62c0aeb7aaf8b3080bab85ce412adff039 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 25 Nov 2021 13:49:06 +0000 Subject: [PATCH 057/128] Remove HttpHeaders dependency in OkHttpClientAdapter (close #266) --- .../snowplow/tracker/http/OkHttpClientAdapter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 9906e8b0..585a7200 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -26,7 +26,6 @@ import okhttp3.RequestBody; // Slf4j -import org.apache.http.HttpHeaders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,7 +130,7 @@ public int doPost(String url, String payload) { RequestBody body = RequestBody.create(payload, JSON); Request request = new Request.Builder() .url(url) - .addHeader(HttpHeaders.CONTENT_TYPE, Constants.POST_CONTENT_TYPE) + .addHeader("Content-Type", Constants.POST_CONTENT_TYPE) .post(body) .build(); try (Response response = httpClient.newCall(request).execute()) { From d1a2768440d5be33c8973737ddfc07ab6fb9bbe3 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 25 Nov 2021 13:49:47 +0000 Subject: [PATCH 058/128] Update gradle GH Action to include Java 17 (close #273) --- .github/workflows/gradle.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a053a7d6..907c2f04 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -4,18 +4,23 @@ name: Build on: [ push ] jobs: - build: + prepare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + build: + needs: prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: - # build and test against LTS releases and latest GA - java: [ 8, 11, 13 ] + # build and test against LTS releases + java: [ 8, 11, 17 ] steps: - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 - name: Set up JDK uses: actions/setup-java@v1 From a7e7d3b79336a3b4005ef0ea8c32494cb33420f3 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 25 Nov 2021 14:00:04 +0000 Subject: [PATCH 059/128] Manually set the session_id (close #265) * Manually set the session_id (close #265) * Make some test names more readable --- .../snowplow/tracker/Subject.java | 23 +++++++++++++++++++ .../snowplow/tracker/constants/Parameter.java | 1 + .../snowplow/tracker/SubjectTest.java | 11 +++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index 5f9d0e64..d8409cdb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -42,6 +42,7 @@ private Subject(SubjectBuilder builder) { this.setUseragent(builder.useragent); this.setNetworkUserId(builder.networkUserId); this.setDomainUserId(builder.domainUserId); + this.setDomainSessionId(builder.domainSessionId); } /** @@ -69,6 +70,7 @@ public static class SubjectBuilder { private String useragent; // Optional private String networkUserId; // Optional private String domainUserId; // Optional + private String domainSessionId; // Optional /** * @param userId a user id string @@ -164,6 +166,15 @@ public SubjectBuilder domainUserId(String domainUserId) { return this; } + /** + * @param domainSessionId a domainSessionId string + * @return itself + */ + public SubjectBuilder domainSessionId(String domainSessionId) { + this.domainSessionId = domainSessionId; + return this; + } + /** * Creates a new Subject * @@ -280,6 +291,18 @@ public void setDomainUserId(String domainUserId) { } } + /** + * User inputted Domain Session ID for the + * subject. + * + * @param domainSessionId a domain session id + */ + public void setDomainSessionId(String domainSessionId) { + if (domainSessionId != null) { + this.standardPairs.put(Parameter.SESSION_UID, domainSessionId); + } + } + /** * User inputted Network User Id for the * subject. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index 1202985a..68d2097c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -49,6 +49,7 @@ public class Parameter { public static final String USERAGENT = "ua"; public static final String DOMAIN_UID = "duid"; public static final String NETWORK_UID = "tnuid"; + public static final String SESSION_UID = "sid"; // Page View public static final String PAGE_URL = "url"; diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 3a293c9d..2593ee8e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -79,19 +79,26 @@ public void testSetUseragent() throws Exception { } @Test - public void testSetDuid() throws Exception { + public void testSetDomainUserId() throws Exception { Subject subject = new Subject.SubjectBuilder().build(); subject.setDomainUserId("duid"); assertEquals("duid", subject.getSubject().get("duid")); } @Test - public void testSetNuid() throws Exception { + public void testSetNetworkUserId() throws Exception { Subject subject = new Subject.SubjectBuilder().build(); subject.setNetworkUserId("nuid"); assertEquals("nuid", subject.getSubject().get("tnuid")); } + @Test + public void testSetDomainSessionId() throws Exception { + Subject subject = new Subject.SubjectBuilder().build(); + subject.setDomainSessionId("sessionid"); + assertEquals("sessionid", subject.getSubject().get("sid")); + } + @Test public void testGetSubject() throws Exception { Subject subject = new Subject.SubjectBuilder().build(); From 7aff3c92338b3b642ea20d8b766891b326498c14 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 29 Nov 2021 14:30:12 +0000 Subject: [PATCH 060/128] Update dependencies guava, wiremock, and httpclient (close #269) * Update guava * Update wiremock and fix junit * Update httpclient * Update mockwebserver --- build.gradle | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 5aaf39af..c3cdea59 100644 --- a/build.gradle +++ b/build.gradle @@ -31,8 +31,6 @@ def javaVersion = JavaVersion.VERSION_1_8 repositories { // Use 'maven central' for resolving our dependencies mavenCentral() - // Use 'jcenter' for resolving testing dependencies - jcenter() } configure([compileJava, compileTestJava]) { @@ -62,7 +60,7 @@ dependencies { api 'commons-net:commons-net:3.6' // Apache HTTP - apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.12' + apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.13' apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.4' // Square OK HTTP @@ -76,17 +74,16 @@ dependencies { api 'com.fasterxml.jackson.core:jackson-databind:2.11.0' // Preconditions - api 'com.google.guava:guava:29.0-jre' + api 'com.google.guava:guava:31.0-jre' // Testing libraries - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' - testCompileOnly 'junit:junit:4.13' - testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'com.github.tomakehurst:wiremock:2.26.3' + testImplementation 'com.github.tomakehurst:wiremock-jre8:2.31.0' testImplementation 'org.mockito:mockito-core:3.3.3' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.7.2' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' } task sourceJar(type: Jar, dependsOn: 'generateSources') { From b5711c413050f3f3961e3875e3ef663ce62d24a9 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 29 Nov 2021 15:46:20 +0000 Subject: [PATCH 061/128] Remove Mockito and Wiremock dependencies (close #275) * Remove wiremock * Remove mockito * Replace deprecated Exception tests --- build.gradle | 2 - .../snowplow/tracker/SubjectTest.java | 24 ++-- .../snowplow/tracker/TrackerTest.java | 125 +++++++++--------- .../snowplow/tracker/UtilsTest.java | 11 +- .../emitter/BatchEmitterBuilderTest.java | 47 ++++--- .../tracker/emitter/BatchEmitterTest.java | 83 +++++++----- .../tracker/http/HttpClientAdapterTest.java | 16 +-- .../tracker/payload/TrackerPayloadTest.java | 13 +- 8 files changed, 174 insertions(+), 147 deletions(-) diff --git a/build.gradle b/build.gradle index c3cdea59..4bb271ff 100644 --- a/build.gradle +++ b/build.gradle @@ -81,8 +81,6 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'com.github.tomakehurst:wiremock-jre8:2.31.0' - testImplementation 'org.mockito:mockito-core:3.3.3' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 2593ee8e..b45ee37b 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -23,84 +23,84 @@ public class SubjectTest { @Test - public void testSetUserId() throws Exception { + public void testSetUserId() { Subject subject = new Subject.SubjectBuilder().build(); subject.setUserId("user1"); assertEquals("user1", subject.getSubject().get("uid")); } @Test - public void testSetScreenResolution() throws Exception { + public void testSetScreenResolution() { Subject subject = new Subject.SubjectBuilder().build(); subject.setScreenResolution(100, 150); assertEquals("100x150", subject.getSubject().get("res")); } @Test - public void testSetViewPort() throws Exception { + public void testSetViewPort() { Subject subject = new Subject.SubjectBuilder().build(); subject.setViewPort(150, 100); assertEquals("150x100", subject.getSubject().get("vp")); } @Test - public void testSetColorDepth() throws Exception { + public void testSetColorDepth() { Subject subject = new Subject.SubjectBuilder().build(); subject.setColorDepth(10); assertEquals("10", subject.getSubject().get("cd")); } @Test - public void testSetTimezone2() throws Exception { + public void testSetTimezone2() { Subject subject = new Subject.SubjectBuilder().build(); subject.setTimezone("America/Toronto"); assertEquals("America/Toronto", subject.getSubject().get("tz")); } @Test - public void testSetLanguage() throws Exception { + public void testSetLanguage() { Subject subject = new Subject.SubjectBuilder().build(); subject.setLanguage("EN"); assertEquals("EN", subject.getSubject().get("lang")); } @Test - public void testSetIpAddress() throws Exception { + public void testSetIpAddress() { Subject subject = new Subject.SubjectBuilder().build(); subject.setIpAddress("127.0.0.1"); assertEquals("127.0.0.1", subject.getSubject().get("ip")); } @Test - public void testSetUseragent() throws Exception { + public void testSetUseragent() { Subject subject = new Subject.SubjectBuilder().build(); subject.setUseragent("useragent"); assertEquals("useragent", subject.getSubject().get("ua")); } @Test - public void testSetDomainUserId() throws Exception { + public void testSetDomainUserId() { Subject subject = new Subject.SubjectBuilder().build(); subject.setDomainUserId("duid"); assertEquals("duid", subject.getSubject().get("duid")); } @Test - public void testSetNetworkUserId() throws Exception { + public void testSetNetworkUserId() { Subject subject = new Subject.SubjectBuilder().build(); subject.setNetworkUserId("nuid"); assertEquals("nuid", subject.getSubject().get("tnuid")); } @Test - public void testSetDomainSessionId() throws Exception { + public void testSetDomainSessionId() { Subject subject = new Subject.SubjectBuilder().build(); subject.setDomainSessionId("sessionid"); assertEquals("sessionid", subject.getSubject().get("sid")); } @Test - public void testGetSubject() throws Exception { + public void testGetSubject() { Subject subject = new Subject.SubjectBuilder().build(); Map expected = new HashMap<>(); subject.setTimezone("America/Toronto"); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 9fe78f48..922d79f2 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -17,42 +17,54 @@ import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import com.google.common.collect.ImmutableMap; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import static org.mockito.Mockito.*; - +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; -@RunWith(MockitoJUnitRunner.class) public class TrackerTest { public static final String EXPECTED_CONTEXTS = "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1\",\"data\":[{\"schema\":\"schema\",\"data\":{\"foo\":\"bar\"}}]}"; public static final String EXPECTED_EVENT_ID = "15e9b149-6029-4f6e-8447-5b9797c9e6be"; - @Mock - Emitter emitter; + public static class MockEmitter implements Emitter { + public ArrayList eventList = new ArrayList<>(); + + @Override + public void emit(TrackerEvent event) { + eventList.add(event); + } + + @Override + public void setBufferSize(int bufferSize) {} - @Captor - ArgumentCaptor captor; + @Override + public void flushBuffer() {} + @Override + public int getBufferSize() { + return 0; + } + + @Override + public List getBuffer() { + return null; + } + } + + MockEmitter mockEmitter; Tracker tracker; private List contexts; @Before - public void setUp() throws Exception { - tracker = new Tracker.TrackerBuilder(emitter, "AF003", "cloudfront") + public void setUp() { + mockEmitter = new MockEmitter(); + tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") .subject(new Subject.SubjectBuilder().build()) .base64(false) .build(); @@ -98,12 +110,10 @@ public void testEcommerceEvent() { .build()); // Then - verify(emitter, times(1)).emit(captor.capture()); - List allValues = captor.getAllValues(); - - assertEquals(allValues.get(0).getTrackerPayloads().size(), 2); + List results = mockEmitter.eventList.get(0).getTrackerPayloads(); + assertEquals(2, results.size()); - Map result1 = allValues.get(0).getTrackerPayloads().get(0).getMap(); + Map result1 = results.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("e", "tr") .put("tr_cu", "currency") @@ -126,7 +136,7 @@ public void testEcommerceEvent() { .put("tr_st", "state") .build(), result1); - Map result2 = allValues.get(0).getTrackerPayloads().get(1).getMap(); + Map result2 = results.get(1).getMap(); assertEquals(ImmutableMap.builder() .put("ti_nm", "name") .put("ti_id", "order_id") @@ -163,9 +173,7 @@ public void testUnstructuredEventWithContext() { .build()); // Then - verify(emitter).emit(captor.capture()); - - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -195,8 +203,7 @@ public void testUnstructuredEventWithoutContext() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -224,8 +231,7 @@ public void testUnstructuredEventWithoutTrueTimestamp() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -241,6 +247,12 @@ public void testUnstructuredEventWithoutTrueTimestamp() { @Test public void testTrackPageView() { + tracker = new Tracker.TrackerBuilder(this.mockEmitter, "AF003", "cloudfront") + .subject(new Subject.SubjectBuilder().build()) + .base64(false) + .build(); + tracker.getSubject().setTimezone("Etc/UTC"); + // When tracker.track(PageView.builder() .pageUrl("url") @@ -253,8 +265,7 @@ public void testTrackPageView() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -294,9 +305,10 @@ public void testTrackTwoEvents() { .build()); // Then - verify(emitter, times(2)).emit(captor.capture()); + List results = mockEmitter.eventList; + assertEquals(2, results.size()); - Map result = captor.getAllValues().get(0).getTrackerPayloads().get(0).getMap(); + Map result1 = results.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -310,9 +322,9 @@ public void testTrackTwoEvents() { .put("aid", "cloudfront") .put("refr", "referer") .put("url", "url") - .build(), result); + .build(), result1); - Map result2 = captor.getAllValues().get(1).getTrackerPayloads().get(0).getMap(); + Map result2 = results.get(1).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -342,8 +354,7 @@ public void testTrackScreenView() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -371,8 +382,7 @@ public void testTrackScreenViewWithTimestamp() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -400,8 +410,7 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -432,8 +441,7 @@ public void testTrackTiming() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -470,8 +478,7 @@ public void testTrackTimingWithSubject() { .build()); // Then - verify(emitter).emit(captor.capture()); - Map result = captor.getValue().getTrackerPayloads().get(0).getMap(); + Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") @@ -491,24 +498,24 @@ public void testTrackTimingWithSubject() { // --- Tracker Setter & Getter Tests @Test - public void testGetTrackerVersion() throws Exception { - Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); + public void testGetTrackerVersion() { + Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); assertEquals("java-0.10.1", tracker.getTrackerVersion()); } @Test - public void testSetDefaultPlatform() throws Exception { - Tracker tracker = new Tracker.TrackerBuilder(emitter, "AF003", "cloudfront") + public void testSetDefaultPlatform() { + Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") .platform(DevicePlatform.Desktop) .build(); assertEquals(DevicePlatform.Desktop, tracker.getPlatform()); } @Test - public void testSetSubject() throws Exception { + public void testSetSubject() { TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); Subject s1 = new Subject.SubjectBuilder().build(); - Tracker tracker = new Tracker.TrackerBuilder(emitter, "AF003", "cloudfront") + Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") .subject(s1) .build(); Subject s2 = new Subject.SubjectBuilder().build(); @@ -521,22 +528,22 @@ public void testSetSubject() throws Exception { } @Test - public void testSetBase64Encoded() throws Exception { - Tracker tracker = new Tracker.TrackerBuilder(emitter, "AF003", "cloudfront") + public void testSetBase64Encoded() { + Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") .base64(false) .build(); - assertTrue(!tracker.getBase64Encoded()); + assertFalse(tracker.getBase64Encoded()); } @Test - public void testSetAppId() throws Exception { - Tracker tracker = new Tracker.TrackerBuilder(emitter, "AF003", "an-app-id").build(); + public void testSetAppId() { + Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "an-app-id").build(); assertEquals("an-app-id", tracker.getAppId()); } @Test - public void testSetNamespace() throws Exception { - Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "an-app-id").build(); + public void testSetNamespace() { + Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); assertEquals("namespace", tracker.getNamespace()); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index e98fec71..aaad8e24 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -14,22 +14,21 @@ // JUnit import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; // Java import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; +import static org.junit.Assert.*; + public class UtilsTest { @Test public void testGetTimestamp() { String ts = Utils.getTimestamp(); assertNotNull(ts); - assertTrue(ts.length() == 13); + assertEquals(13, ts.length()); } @Test @@ -55,9 +54,9 @@ public void testIsUriValid() { assertTrue(Utils.isValidUrl(goodUri3)); String badUri1 = "www.acme.com"; - assertTrue(!Utils.isValidUrl(badUri1)); + assertFalse(Utils.isValidUrl(badUri1)); String badUri2 = "http://"; - assertTrue(!Utils.isValidUrl(badUri2)); + assertFalse(Utils.isValidUrl(badUri2)); } @Test diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java index af479c9a..7dcc5e74 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -12,36 +12,51 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; +import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import org.junit.Assert; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.mockito.Mockito.*; - -import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; public class BatchEmitterBuilderTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Test - public void setNeitherHttpClientAdapterOrCollectorUrl_shouldThrowException() throws Exception { - expectedException.expect(Exception.class); - BatchEmitter.builder().build(); + public void setNeitherHttpClientAdapterOrCollectorUrl_shouldThrowException() { + Exception exception = Assert.assertThrows(Exception.class, () -> BatchEmitter.builder().build()); + Assert.assertEquals("Collector url must be specified if not using a httpClientAdapter", exception.getMessage()); } @Test - public void setCollectorUrlAndNoHttpClientAdapter_shouldInitialiseCorrectly() throws Exception { + public void setCollectorUrlAndNoHttpClientAdapter_shouldInitialiseCorrectly() { BatchEmitter emitter = BatchEmitter.builder().url("https://mycollector.com").build(); Assert.assertNotNull(emitter); } @Test - public void setHttpClientAdapterAndNoCollectorUrl_shouldInitialiseCorrectly() throws Exception { - HttpClientAdapter httpClientAdapter = mock(HttpClientAdapter.class); - BatchEmitter emitter = BatchEmitter.builder().httpClientAdapter(httpClientAdapter).build(); + public void setHttpClientAdapterAndNoCollectorUrl_shouldInitialiseCorrectly() { + HttpClientAdapter mockHttpClientAdapter = new HttpClientAdapter() { + @Override + public int post(SelfDescribingJson payload) { + return 0; + } + + @Override + public int get(TrackerPayload payload) { + return 0; + } + + @Override + public String getUrl() { + return null; + } + + @Override + public Object getHttpClient() { + return null; + } + }; + + BatchEmitter emitter = BatchEmitter.builder().httpClientAdapter(mockHttpClientAdapter).build(); Assert.assertNotNull(emitter); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 302ea57d..aa897a60 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -20,12 +20,7 @@ import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.mockito.ArgumentCaptor; -import static org.mockito.Mockito.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -41,26 +36,50 @@ public class BatchEmitterTest { - private HttpClientAdapter httpClientAdapter; + private MockHttpClientAdapter mockHttpClientAdapter; private BatchEmitter emitter; - @Rule - public ExpectedException expectedException = ExpectedException.none(); + public static class MockHttpClientAdapter implements HttpClientAdapter { + public boolean isGetCalled = false; + public boolean isPostCalled = false; + public SelfDescribingJson capturedPayload; + + @Override + public int post(SelfDescribingJson payload) { + isPostCalled = true; + capturedPayload = payload; + return 200; + } + + @Override + public int get(TrackerPayload payload) { + isGetCalled = true; + return 0; + } + + @Override + public String getUrl() { + return null; + } + + @Override + public Object getHttpClient() { + return null; + } + } @Before - public void setUp() throws Exception { - httpClientAdapter = mock(HttpClientAdapter.class); - emitter = spy(BatchEmitter.builder() - .httpClientAdapter(httpClientAdapter) + public void setUp() { + mockHttpClientAdapter = new MockHttpClientAdapter(); + emitter = BatchEmitter.builder() + .httpClientAdapter(mockHttpClientAdapter) .bufferSize(10) - .build()); + .build(); } @Test - public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws Exception { + public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws InterruptedException { // Given - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(TrackerPayload.class); - List events = createEvents(2); // When @@ -71,16 +90,15 @@ public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws Excepti Thread.sleep(500); // Then - verify(httpClientAdapter, never()).get(argumentCaptor.capture()); + Assert.assertFalse(mockHttpClientAdapter.isGetCalled); Assert.assertEquals(2, emitter.getBuffer().size()); Assert.assertEquals(events, emitter.getBuffer()); } @Test - public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Exception { + public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws InterruptedException { // Given - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); List events = createEvents(10); // When @@ -91,10 +109,10 @@ public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Exception Thread.sleep(500); // Then - verify(httpClientAdapter).post(argumentCaptor.capture()); + Assert.assertTrue(mockHttpClientAdapter.isPostCalled); @SuppressWarnings("unchecked") - List> capturedPayload = (List>) argumentCaptor.getValue().getMap().get("data"); + List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); assertPayload(events, capturedPayload); @@ -102,10 +120,8 @@ public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Exception } @Test - public void flushBuffer_shouldEmptyBuffer() throws Exception { + public void flushBuffer_shouldEmptyBuffer() throws InterruptedException { // Given - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); - List events = createEvents(2); // When @@ -118,10 +134,10 @@ public void flushBuffer_shouldEmptyBuffer() throws Exception { Thread.sleep(500); // Then - verify(httpClientAdapter).post(argumentCaptor.capture()); + Assert.assertTrue(mockHttpClientAdapter.isPostCalled); @SuppressWarnings("unchecked") - List> capturedPayload = (List>) argumentCaptor.getValue().getMap().get(Parameter.DATA); + List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); assertPayload(events, capturedPayload); @@ -129,15 +145,14 @@ public void flushBuffer_shouldEmptyBuffer() throws Exception { } @Test - public void setBufferSize_WithNegativeValue_ThrowInvalidArgumentException() throws Exception { - expectedException.expect(IllegalArgumentException.class); - emitter.setBufferSize(-1); + public void setBufferSize_WithNegativeValue_ThrowInvalidArgumentException() { + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> emitter.setBufferSize(-1)); + Assert.assertEquals("bufferSize must be greater than 0", exception.getMessage()); } @Test - public void getFinalPost_shouldAddSTMParameter() throws Exception { + public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { // Given - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SelfDescribingJson.class); List events = createEvents(10); // When @@ -148,10 +163,10 @@ public void getFinalPost_shouldAddSTMParameter() throws Exception { Thread.sleep(500); // Then - verify(httpClientAdapter).post(argumentCaptor.capture()); + Assert.assertTrue(mockHttpClientAdapter.isPostCalled); @SuppressWarnings("unchecked") - List> capturedPayload = (List>) argumentCaptor.getValue().getMap().get(Parameter.DATA); + List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); for (Map payloadMap : capturedPayload) { Assert.assertTrue(payloadMap.containsKey(Parameter.DEVICE_SENT_TIMESTAMP)); @@ -193,7 +208,7 @@ private void assertPayload(List events, List> //Assert that all the entries in the event are in the captured payload //There might be extra entries in capturedMap, such as the STM parameter - //check for these addtional parameters in other tests + //check for these additional parameters in other tests assertThat(eventMap.entrySet(), everyItem(is(in(capturedMap.entrySet())))); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index ac1f3790..41aaa78a 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -26,9 +26,8 @@ import org.apache.http.impl.client.HttpClients; -import org.junit.Rule; +import org.junit.Assert; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import static org.junit.Assert.assertEquals; @@ -38,9 +37,6 @@ @RunWith(Parameterized.class) public class HttpClientAdapterTest { - - @Rule - public ExpectedException expectedException = ExpectedException.none(); private final MockWebServer mockWebServer; private HttpClientAdapter adapter; @@ -121,14 +117,12 @@ public void post_withSuccessfulStatusCode_isOk() throws InterruptedException { } @Test - public void testPostWithNullArgument() throws Exception { - expectedException.expect(NullPointerException.class); - adapter.post(null); + public void testPostWithNullArgument() { + Assert.assertThrows(NullPointerException.class, () -> adapter.post(null)); } @Test - public void testGetWithNullArgument() throws Exception { - expectedException.expect(NullPointerException.class); - adapter.get(null); + public void testGetWithNullArgument() { + Assert.assertThrows(NullPointerException.class, () -> adapter.get(null)); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java index 38235d6e..32659b49 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java @@ -18,9 +18,8 @@ // JUnit import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; + +import static org.junit.Assert.*; public class TrackerPayloadTest { @@ -38,7 +37,7 @@ public void testAddKeyWithNullValue() { TrackerPayload payload = new TrackerPayload(); payload.add("key", null); assertNotNull(payload); - assertTrue(!payload.getMap().containsKey("key")); + assertFalse(payload.getMap().containsKey("key")); } @Test @@ -46,7 +45,7 @@ public void testAddKeyWithEmptyValue() { TrackerPayload payload = new TrackerPayload(); payload.add("key", ""); assertNotNull(payload); - assertTrue(!payload.getMap().containsKey("key")); + assertFalse(payload.getMap().containsKey("key")); } @Test @@ -67,7 +66,7 @@ public void testAddMapWithNullValue() { TrackerPayload payload = new TrackerPayload(); payload.addMap(data); assertNotNull(payload); - assertTrue(!payload.getMap().containsKey("key")); + assertFalse(payload.getMap().containsKey("key")); } @Test @@ -77,7 +76,7 @@ public void testAddMapWithEmptyValue() { TrackerPayload payload = new TrackerPayload(); payload.addMap(data); assertNotNull(payload); - assertTrue(!payload.getMap().containsKey("key")); + assertFalse(payload.getMap().containsKey("key")); } @Test From f19fcb4c00f63db931c3775580278cc414015215 Mon Sep 17 00:00:00 2001 From: Buck Ryan Date: Tue, 30 Nov 2021 06:08:53 -0500 Subject: [PATCH 062/128] Specify the key for 'null or empty value detected' payload log (close #277) --- .../snowplow/tracker/payload/TrackerPayload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 61b8efa3..2cd187fe 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -44,7 +44,7 @@ public void add(final String key, final String value) { return; } if (value == null || value.isEmpty()) { - LOGGER.info("null or empty value detected: {}", value); + LOGGER.info("null or empty value detected: {}->{}", key, value); return; } LOGGER.debug("Adding new kv pair: {}->{}", key, value); From ba2cd192168f78d41750e9f00faad66b9a35915a Mon Sep 17 00:00:00 2001 From: Paul Laturaze Date: Tue, 30 Nov 2021 12:19:04 +0100 Subject: [PATCH 063/128] Allow Emitter to use a custom ExecutorService (close #278) This commit allows a user to use its own ExecutorService in the Emitter to have a more granular control over threads created by an Emitter. Co-authored-by: Paul Laturaze --- .../tracker/emitter/AbstractEmitter.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index e200c140..e8502fa8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import com.google.common.base.Preconditions; @@ -40,9 +39,22 @@ public static abstract class Builder> { private HttpClientAdapter httpClientAdapter; // Optional private RequestCallback requestCallback = null; // Optional private int threadCount = 50; // Optional + private ExecutorService requestExecutorService = null; // Optional private String collectorUrl = null; // Required if not specifying a httpClientAdapter protected abstract T self(); + /** + * Set a custom ExecutorService to send http request. + * + * /!\ Be aware that calling `close()` on a BatchEmitter instance has a side-effect and will shutdown that ExecutorService. + * @param executorService the ExecutorService to use + * @return itself + */ + public T requestExecutorService(final ExecutorService executorService) { + this.requestExecutorService = executorService; + return self(); + } + /** * Adds the HttpClientAdapter to the AbstractEmitter * @@ -119,7 +131,11 @@ protected AbstractEmitter(final Builder builder) { } this.requestCallback = builder.requestCallback; - this.executor = Executors.newScheduledThreadPool(builder.threadCount); + if (builder.requestExecutorService != null) { + this.executor = builder.requestExecutorService; + } else { + this.executor = Executors.newScheduledThreadPool(builder.threadCount); + } } /** From c95e5740432e4d09d00e82e2629f0161b5759aac Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 2 Dec 2021 17:12:08 +0000 Subject: [PATCH 064/128] Update all copyright notices (close #279) * Update copyright notices to 2021 * Update LICENSE file --- LICENSE-2.0.txt => LICENSE | 2 +- build.gradle | 4 ++-- .../src/main/java/com/snowplowanalytics/Main.java | 2 +- .../src/test/java/com/snowplowanalytics/MainTest.java | 2 +- .../snowplowanalytics/snowplow/tracker/DevicePlatform.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Subject.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Tracker.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Utils.java | 2 +- .../snowplow/tracker/constants/Constants.java | 2 +- .../snowplow/tracker/constants/Parameter.java | 2 +- .../snowplow/tracker/emitter/AbstractEmitter.java | 2 +- .../snowplow/tracker/emitter/BatchEmitter.java | 2 +- .../snowplowanalytics/snowplow/tracker/emitter/Emitter.java | 2 +- .../snowplow/tracker/emitter/RequestCallback.java | 2 +- .../snowplow/tracker/emitter/SimpleEmitter.java | 2 +- .../snowplow/tracker/events/AbstractEvent.java | 2 +- .../snowplow/tracker/events/EcommerceTransaction.java | 2 +- .../snowplow/tracker/events/EcommerceTransactionItem.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Event.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/PageView.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/ScreenView.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/Structured.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Timing.java | 2 +- .../snowplow/tracker/events/Unstructured.java | 2 +- .../snowplow/tracker/http/AbstractHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/ApacheHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/HttpClientAdapter.java | 2 +- .../snowplow/tracker/http/OkHttpClientAdapter.java | 2 +- .../snowplowanalytics/snowplow/tracker/payload/Payload.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJson.java | 2 +- .../snowplow/tracker/payload/TrackerEvent.java | 2 +- .../snowplow/tracker/payload/TrackerParameters.java | 2 +- .../snowplow/tracker/payload/TrackerPayload.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/SubjectTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/UtilsTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterBuilderTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterTest.java | 2 +- .../snowplow/tracker/http/HttpClientAdapterTest.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJsonTest.java | 2 +- .../snowplow/tracker/payload/TrackerPayloadTest.java | 2 +- 41 files changed, 42 insertions(+), 42 deletions(-) rename LICENSE-2.0.txt => LICENSE (99%) diff --git a/LICENSE-2.0.txt b/LICENSE similarity index 99% rename from LICENSE-2.0.txt rename to LICENSE index 7a4a3ea2..b2d6fe1e 100644 --- a/LICENSE-2.0.txt +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2021 Snowplow Analytics Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle b/build.gradle index 4bb271ff..a08c3326 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -96,7 +96,7 @@ task generateSources { srcFile.parentFile.mkdirs() srcFile.write( """/* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 9e65aaa3..6b9f2ab3 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java index 23341b8b..30698085 100644 --- a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java +++ b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java index c04d8f80..b457bfb9 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index d8409cdb..c61958e8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index a2390ede..a992c48c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index 8102814a..39b57cf7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java index 5d3e0b4f..0bf291b4 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index 68d2097c..f00cf1a3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index e8502fa8..bdf29ad4 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index f7e9baa2..a286f356 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index e49aa8d2..c51a456d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java index 217b599b..4df7c8bb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index f78274aa..f3854a0f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 6c366585..31161c0e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 12ef9b63..d7f31752 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index 4d2eb201..0a928f4c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 192943d5..935279a1 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index 11c12c0a..27bed72f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 4d56ce88..d84adc57 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index c1b91c33..e80da843 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index 2ea85904..c005d158 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index acc76b53..a73b4575 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index 959891e7..0623966e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index d326917d..121e24a4 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java index f462ceea..bbf00bdf 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 585a7200..3e0a63e6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java index 312f1ced..e6febee3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index 2be2b640..a41285ea 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java index 10f1661a..ced65783 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java index 5c0264e0..90bac316 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 2cd187fe..a48deeab 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index b45ee37b..b2850d2c 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 922d79f2..1743c9e3 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index aaad8e24..56541957 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java index 7dcc5e74..2bb49acc 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index aa897a60..5890eaba 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 41aaa78a..4152c374 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java index da3151f2..1c831fac 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java index 32659b49..a9ffb90c 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2020 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. From c319f7ba5fdc30b210af25f38590e59c5c6a0774 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 6 Dec 2021 14:40:01 +0000 Subject: [PATCH 065/128] Set Emitter's threads name for easier debugging (close #280) * Set Emitter's threads name for easier debugging (close #280) Add a ThreadFactory to the Emitter executor service to set a custom name for threads that are created when calling execute(). Set a name for the buffer consumer thread in the BatchEmitter. Naming these threads can be useful when investigating a thread dump, or for monitoring purpose. Co-authored-by: Paul Laturaze Co-authored-by: Paul Laturaze Co-authored-by: Miranda Wilson * Add tests for thread names --- .../tracker/emitter/AbstractEmitter.java | 35 ++++++++++++++++++- .../tracker/emitter/BatchEmitter.java | 10 ++++-- .../tracker/emitter/SimpleEmitter.java | 2 +- .../tracker/emitter/BatchEmitterTest.java | 32 +++++++++++++++++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index bdf29ad4..b1108a4d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; import com.google.common.base.Preconditions; @@ -131,10 +133,11 @@ protected AbstractEmitter(final Builder builder) { } this.requestCallback = builder.requestCallback; + if (builder.requestExecutorService != null) { this.executor = builder.requestExecutorService; } else { - this.executor = Executors.newScheduledThreadPool(builder.threadCount); + this.executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); } } @@ -195,4 +198,34 @@ protected void execute(final Runnable runnable) { protected boolean isSuccessfulSend(final int code) { return code >= 200 && code < 300; } + + /** + * Copied from `Executors.defaultThreadFactory()`. + * The only change is the generated name prefix. + */ + static class EmitterThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + EmitterThreadFactory() { + SecurityManager securityManager = System.getSecurityManager(); + this.group = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); + this.namePrefix = "snowplow-emitter-pool-" + poolNumber.getAndIncrement() + "-request-thread-"; + } + + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(this.group, runnable, this.namePrefix + this.threadNumber.getAndIncrement(), 0L); + if (thread.isDaemon()) { + thread.setDaemon(false); + } + + if (thread.getPriority() != 5) { + thread.setPriority(5); + } + + return thread; + } + } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index a286f356..94a65927 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -20,6 +20,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.concurrent.atomic.AtomicInteger; import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.constants.Constants; @@ -38,6 +39,8 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); + private static final AtomicInteger BUFFER_CONSUMER_THREAD_NUMBER = new AtomicInteger(1); + private static final String BUFFER_CONSUMER_THREAD_NAME_PREFIX = "snowplow-emitter-BufferConsumer-thread-"; private final Thread bufferConsumer; private boolean isClosing = false; @@ -89,7 +92,10 @@ protected BatchEmitter(final Builder builder) { this.bufferSize = builder.bufferSize; - bufferConsumer = new Thread(getBufferConsumerRunnable()); + bufferConsumer = new Thread( + getBufferConsumerRunnable(), + BUFFER_CONSUMER_THREAD_NAME_PREFIX + BUFFER_CONSUMER_THREAD_NUMBER.getAndIncrement() + ); bufferConsumer.start(); } @@ -258,7 +264,7 @@ public void close() { isClosing = true; bufferConsumer.interrupt(); // Kill buffer consumer - flushBuffer(); // Attempt to send all reminaing events + flushBuffer(); // Attempt to send all remaining events //Shutdown executor threadpool if (executor != null) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index f3854a0f..8b7a46df 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -29,7 +29,7 @@ */ public class SimpleEmitter extends AbstractEmitter { - private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SimpleEmitter.class); public static abstract class Builder> extends AbstractEmitter.Builder { public SimpleEmitter build() { diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 5890eaba..cbfb12aa 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -173,6 +173,38 @@ public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { } } + @Test + public void emitterThreadFactory_correctlyNamesThreads() { + class MyRunnable implements Runnable { + @Override + public void run() {} + } + + BatchEmitter.EmitterThreadFactory threadFactory = new BatchEmitter.EmitterThreadFactory(); + String threadName = threadFactory.newThread(new MyRunnable()).getName(); + + // It's pool-2 because pool-1 was created during emitter instantiation + Assert.assertEquals("snowplow-emitter-pool-2-request-thread-1", threadName); + } + + @Test + public void threadsHaveExpectedNames() { + // A BufferConsumer thread is created on BatchEmitter instantiation. + // Calling flushBuffer() here to require another thread - causing + // creation of a request thread within the scheduledThreadPool. + emitter.flushBuffer(); + + // Create a list of all live thread names + List threadList = new ArrayList<>(Thread.getAllStackTraces().keySet()); + List threadNames = new ArrayList<>(); + for (Thread thread : threadList) { + threadNames.add(thread.getName()); + } + + Assert.assertTrue(threadNames.contains("snowplow-emitter-BufferConsumer-thread-1")); + Assert.assertTrue(threadNames.contains("snowplow-emitter-pool-1-request-thread-1")); + } + private List createEvents(int numEvents) { final List payloads = Lists.newArrayList(); for (int i = 0; i < numEvents; i++) { From 8e15c1c5dcd20508ba8a7185b8ea9384eea0c978 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 13 Dec 2021 16:22:17 +0000 Subject: [PATCH 066/128] Update Deploy action to remove Bintray (close #283) * Update deploy action to remove bintray * Update version of gh-release action --- .github/workflows/deploy.yml | 80 +++++++++++++++++++----------------- build.gradle | 67 +++++++++++------------------- 2 files changed, 65 insertions(+), 82 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 28775cd9..be7bd7a8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,49 +1,53 @@ - name: Deploy on: push: tags: - - '*.*.*' - + - '*.*.*' + jobs: deploy: runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 - env: - BINTRAY_SNOWPLOW_MAVEN_USER: ${{ secrets.BINTRAY_SNOWPLOW_MAVEN_USER }} - BINTRAY_SNOWPLOW_MAVEN_API_KEY: ${{ secrets.BINTRAY_SNOWPLOW_MAVEN_API_KEY }} - SONA_USER: 'snowplow' - SONA_PASS: ${{ secrets.SONA_PASS }} + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 - steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 - - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: 8 - - - name: Cache Gradle packages - uses: actions/cache@v2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: ${{ runner.os }}-gradle - - - name: Get tag and tracker version information - id: version - run: | - echo ::set-output name=TAG_VERSION::${GITHUB_REF#refs/*/} - echo "##[set-output name=TRACKER_VERSION;]$(./gradlew -q printVersion)" - - - name: Fail if version mismatch - if: ${{ steps.version.outputs.TAG_VERSION != steps.version.outputs.TRACKER_VERSION }} - run: | - echo "Tag version (${{ steps.version.outputs.TAG_VERSION }}) doesn't match version in project (${{ steps.version.outputs.TRACKER_VERSION }})" - exit 1 - - - name: Publish - run: ./gradlew bintrayUpload + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: Build + run: ./gradlew build + + - name: Get tag and tracker version information + id: version + run: | + echo ::set-output name=TAG_VERSION::${GITHUB_REF#refs/*/} + echo "##[set-output name=TRACKER_VERSION;]$(./gradlew -q printVersion)" + + - name: Fail if version mismatch + if: ${{ steps.version.outputs.TAG_VERSION != steps.version.outputs.TRACKER_VERSION }} + run: | + echo "Tag version (${{ steps.version.outputs.TAG_VERSION }}) doesn't match version in project (${{ steps.version.outputs.TRACKER_VERSION }})" + exit 1 + + - name: Publish to Maven Central + run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository + env: + SONA_USER: ${{ secrets.SONA_USER }} + SONA_PASS: ${{ secrets.SONA_PASS }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONA_PGP_PASSPHRASE }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONA_PGP_SECRET }} + + - name: Release on GitHub + uses: softprops/action-gh-release@v1 + with: + name: Version ${{ steps.version.outputs.TRACKER_VERSION }} + prerelease: ${{ contains(steps.version.outputs.TRACKER_VERSION, '-') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/build.gradle b/build.gradle index a08c3326..386ae8d4 100644 --- a/build.gradle +++ b/build.gradle @@ -11,10 +11,13 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ +import java.time.Duration + plugins { - id 'com.jfrog.bintray' version '1.8.5' id 'java-library' id 'maven-publish' + id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' + id 'signing' id 'idea' } @@ -22,14 +25,13 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.10.1' +version = '0.11.0-alpha.15' sourceCompatibility = '1.8' targetCompatibility = '1.8' def javaVersion = JavaVersion.VERSION_1_8 repositories { - // Use 'maven central' for resolving our dependencies mavenCentral() } @@ -193,49 +195,26 @@ publishing { } } -// Workaround for upload of module.json file, remove when issue fixed https://github.com/bintray/gradle-bintray-plugin/issues/229 -bintrayUpload.doFirst { - publishing.publications.all { publication -> - def moduleFile = file("$buildDir/publications/$publication.name/module.json") - if (moduleFile.exists()) { - artifact(moduleFile) { - extension = "module" - } +nexusPublishing { + repositories { + sonatype { + username = System.getenv('SONA_USER') + password = System.getenv('SONA_PASS') } } + transitionCheckOptions { + maxRetries.set(360) + delayBetween.set(Duration.ofSeconds(20)) + } } -bintray { - user = System.getenv('BINTRAY_SNOWPLOW_MAVEN_USER') - key = System.getenv('BINTRAY_SNOWPLOW_MAVEN_API_KEY') - - publish = true - - pkg { - repo = 'snowplow-maven' - name = 'snowplow-java-tracker' - - group = 'com.snowplowanalytics' - userOrg = 'snowplow' - - websiteUrl = 'https://github.com/snowplow/snowplow-java-tracker' - vcsUrl = 'https://github.com/snowplow/snowplow-java-tracker' - issueTrackerUrl = 'https://github.com/snowplow/snowplow-java-tracker/issues' - - licenses = ['Apache-2.0'] - publications = ['mavenJava'] - - version { - name = "$project.version" - gpg { - sign = true - } - mavenCentralSync { - sync = true - user = System.getenv('SONA_USER') - password = System.getenv('SONA_PASS') - close = '1' - } - } +signing { + if (System.getenv('ORG_GRADLE_PROJECT_signingKey') && System.getenv('ORG_GRADLE_PROJECT_signingPassword')) { + println 'Found signing credentials. Signing...' + def signingKey = findProperty("signingKey") + def signingPassword = findProperty("signingPassword") + useInMemoryPgpKeys(signingKey, signingPassword) + sign publishing.publications.mavenJava + println 'Used useInMemoryPgpKeys()' } -} +} \ No newline at end of file From 7aa80b24f12adfbe777c55542eb8ef90baa5a6cc Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 13 Dec 2021 18:32:08 +0000 Subject: [PATCH 067/128] Remove logging of user supplied values (close #286) * Remove logging of user supplied values (close #286) * Rearrange order of error and debug logging --- .../com/snowplowanalytics/snowplow/tracker/Utils.java | 11 +++++++---- .../snowplow/tracker/payload/TrackerPayload.java | 4 ++-- .../snowplowanalytics/snowplow/tracker/UtilsTest.java | 11 +++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index 39b57cf7..a031e0d3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -77,7 +77,8 @@ public static boolean isValidUrl(String url) { new URL(url).toURI(); return true; } catch (Exception e) { - LOGGER.error("URI {} is not valid: {}", url, e.getMessage()); + LOGGER.error("Invalid URI"); + LOGGER.debug("URI {} is not valid: {}", url, e.getMessage()); return false; } } @@ -120,7 +121,8 @@ public static String mapToJSONString(Map map) { try { jString = objectMapper.writeValueAsString(map); } catch (JsonProcessingException e) { - LOGGER.error("Could not process Map {} into JSON String: {}", map, e.getMessage()); + LOGGER.error("Could not process Map into JSON String"); + LOGGER.debug("Could not process Map {} into JSON String: {}", map, e.getMessage()); } return jString; } @@ -142,7 +144,7 @@ public static String mapToQueryString(Map map) { String encodedVal = urlEncodeUTF8(map.get(key)); // Do not add empty Keys - if (encodedKey != null && !encodedKey.isEmpty()) { + if (!encodedKey.isEmpty()) { sb.append(String.format("%s=%s", encodedKey, encodedVal)); } } @@ -163,7 +165,8 @@ public static String urlEncodeUTF8(Object o) { String encoded = URLEncoder.encode(s, "UTF-8"); return encoded.replaceAll("\\+", "%20"); } catch (Exception e) { - LOGGER.error("Object {} could not be encoded: {}", o, e.getMessage()); + LOGGER.error("Object could not be encoded"); + LOGGER.debug("Object {} could not be encoded: {}", o, e.getMessage()); return ""; } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index a48deeab..93bb9303 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -40,11 +40,11 @@ public class TrackerPayload implements Payload { @Override public void add(final String key, final String value) { if (key == null || key.isEmpty()) { - LOGGER.error("Invalid key detected: {}", key); + LOGGER.error("Null or empty key detected"); return; } if (value == null || value.isEmpty()) { - LOGGER.info("null or empty value detected: {}->{}", key, value); + LOGGER.debug("Null or empty value detected: {}->{}", key, value); return; } LOGGER.debug("Adding new kv pair: {}->{}", key, value); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index 56541957..63906c5e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -59,6 +59,17 @@ public void testIsUriValid() { assertFalse(Utils.isValidUrl(badUri2)); } + @Test + public void testMapToJSONString() { + Map payload = new LinkedHashMap<>(); + payload.put("k1", "v1"); + assertEquals("{\"k1\":\"v1\"}", Utils.mapToJSONString(payload)); + + Map payload2 = new LinkedHashMap<>(); + payload2.put("k1", new Object()); + assertEquals("", Utils.mapToJSONString(payload2)); + } + @Test public void testMapToQueryString() { Map payload = new LinkedHashMap<>(); From e0d092684571e98078c4932e48af2550f5af17a9 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 13 Dec 2021 18:42:29 +0000 Subject: [PATCH 068/128] Prepare for v0.11.0 release --- CHANGELOG | 16 ++++++++++++++++ build.gradle | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 84d079bd..beb7a49c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,19 @@ +Java 0.11.0 (2021-12-14) +----------------------- +Remove logging of user supplied values (#286) +Update Deploy action to remove Bintray (#283) +Set Emitter's threads name for easier debugging (#280) +Update all copyright notices (#279) +Allow Emitter to use a custom ExecutorService (#278) +Specify the key for 'null or empty value detected' payload log (#277) +Remove Mockito and Wiremock dependencies (#275) +Update dependencies guava, wiremock, and httpclient (#269) +Manually set the session_id (#265) +Update gradle GH Action to include Java 17 (#273) +Remove HttpHeaders dependency in OkHttpClientAdapter (#266) +Replace Vagrant with Docker (#267) + + Java 0.10.1 (2020-06-11) ----------------------- Publish Gradle module file with bintrayUpload (#255) diff --git a/build.gradle b/build.gradle index 386ae8d4..6a1eb761 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.11.0-alpha.15' +version = '0.11.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' @@ -217,4 +217,4 @@ signing { sign publishing.publications.mavenJava println 'Used useInMemoryPgpKeys()' } -} \ No newline at end of file +} From 117995a1a5b551abcdbed015425282b30f4c79ea Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 16 Dec 2021 15:08:33 +0000 Subject: [PATCH 069/128] Attribute community contributions in changelog (close #289) --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index beb7a49c..c5c176e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,10 +2,10 @@ Java 0.11.0 (2021-12-14) ----------------------- Remove logging of user supplied values (#286) Update Deploy action to remove Bintray (#283) -Set Emitter's threads name for easier debugging (#280) +Set Emitter's threads name for easier debugging (#280) (Thanks @AcidFlow!) Update all copyright notices (#279) -Allow Emitter to use a custom ExecutorService (#278) -Specify the key for 'null or empty value detected' payload log (#277) +Allow Emitter to use a custom ExecutorService (#278) (Thanks @AcidFlow!) +Specify the key for 'null or empty value detected' payload log (#277) (Thanks @b-ryan!) Remove Mockito and Wiremock dependencies (#275) Update dependencies guava, wiremock, and httpclient (#269) Manually set the session_id (#265) From 273ad514cca69d4fd7d7e83f3822fe961413f88f Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 12 Jan 2022 15:15:34 +0000 Subject: [PATCH 070/128] Extract event storage from Emitter (close #290) * Create EventStore interface, start moving methods * Move BufferConsumer thread to EventStore * Move event buffer event flushing to EventStore * Restore correct version of Tracker * Use a better thread name * Streamline EventStore interface * Improve EventStore method return type * Add unit tests for InMemoryEventStore * Remove unwanted comments and changes --- .../tracker/emitter/BatchEmitter.java | 131 +++++++----------- .../snowplow/tracker/emitter/EventStore.java | 17 +++ .../tracker/emitter/InMemoryEventStore.java | 36 +++++ .../tracker/emitter/SimpleEmitter.java | 4 +- .../tracker/emitter/BatchEmitterTest.java | 70 ++++++---- .../emitter/InMemoryEventStoreTest.java | 102 ++++++++++++++ 6 files changed, 253 insertions(+), 107 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java create mode 100644 src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 94a65927..377c1e8d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -16,8 +16,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.concurrent.atomic.AtomicInteger; @@ -39,25 +37,21 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); - private static final AtomicInteger BUFFER_CONSUMER_THREAD_NUMBER = new AtomicInteger(1); - private static final String BUFFER_CONSUMER_THREAD_NAME_PREFIX = "snowplow-emitter-BufferConsumer-thread-"; + private static final AtomicInteger EVENTS_CHECK_THREAD_NUMBER = new AtomicInteger(1); + private static final String EVENTS_CHECK_THREAD_NAME_PREFIX = "snowplow-emitter-checkForEvents-thread-"; - private final Thread bufferConsumer; + private final Thread checkForEventsToSend; private boolean isClosing = false; private int bufferSize = 1; - - // Queue for immediate buffering of events - private final BlockingQueue eventBuffer = new LinkedBlockingQueue<>(); - - // Queue for storing events until bufferSize is reached - private final BlockingQueue eventsToSend = new LinkedBlockingQueue<>(); + private final EventStore eventStore; private final long closeTimeout = 5; public static abstract class Builder> extends AbstractEmitter.Builder { private int bufferSize = 50; // Optional + private EventStore eventStore = new InMemoryEventStore(); /** * @param bufferSize The count of events to buffer before sending @@ -68,6 +62,11 @@ public T bufferSize(final int bufferSize) { return self(); } + public T eventStore(final EventStore eventStore) { + this.eventStore = eventStore; + return self(); + } + public BatchEmitter build() { return new BatchEmitter(this); } @@ -91,12 +90,13 @@ protected BatchEmitter(final Builder builder) { Preconditions.checkArgument(builder.bufferSize > 0, "bufferSize must be greater than 0"); this.bufferSize = builder.bufferSize; + this.eventStore = builder.eventStore; - bufferConsumer = new Thread( - getBufferConsumerRunnable(), - BUFFER_CONSUMER_THREAD_NAME_PREFIX + BUFFER_CONSUMER_THREAD_NUMBER.getAndIncrement() + checkForEventsToSend = new Thread( + getCheckForEventsToSendRunnable(), + EVENTS_CHECK_THREAD_NAME_PREFIX + EVENTS_CHECK_THREAD_NUMBER.getAndIncrement() ); - bufferConsumer.start(); + checkForEventsToSend.start(); } /** @@ -106,7 +106,7 @@ protected BatchEmitter(final Builder builder) { */ @Override public void emit(final TrackerEvent event) { - boolean result = eventBuffer.offer(event); // Add to buffer and quickly return back to application + boolean result = eventStore.add(event); if (!result) { LOGGER.error("Unable to add event to emitter, emitter buffer is full"); @@ -114,21 +114,11 @@ public void emit(final TrackerEvent event) { } /* - * Forces the events currently in the buffer to be sent + * Forces all the events currently in the buffer to be sent */ @Override public void flushBuffer() { - // Drain immediate event buffer - while (true) { - TrackerEvent event = eventBuffer.poll(); - if (event == null) { - break; - } else { - eventsToSend.offer(event); - } - } - - drainBufferAndSend(); + drainEventsAndSend(eventStore.getSize()); } /** @@ -138,7 +128,7 @@ public void flushBuffer() { */ @Override public List getBuffer() { - return eventsToSend.stream().collect(Collectors.toList()); + return eventStore.getAllEvents(); } /** @@ -163,35 +153,23 @@ public int getBufferSize() { } /** - * Returns a Consumer for the concurrent queue buffer - * Consumes events onto another queue to be sent when bufferSize is reached + * Checks if bufferSize is reached * * @return the new Runnable object */ - private Runnable getBufferConsumerRunnable() { - return new Runnable() { - @Override - public void run() { - while (true) { - try { - eventsToSend.put(eventBuffer.take()); - if (eventsToSend.size() >= bufferSize) { - drainBufferAndSend(); - } - } catch (InterruptedException ex) { - if (isClosing) { - return; - } - } + private Runnable getCheckForEventsToSendRunnable() { + return () -> { + while (!isClosing) { + if (eventStore.getSize() >= bufferSize) { + drainEventsAndSend(getBufferSize()); } } }; } - private void drainBufferAndSend() { - List events = new ArrayList<>(); - eventsToSend.drainTo(events); - execute(getRequestRunnable(events)); + private void drainEventsAndSend(int numberOfEvents) { + List events = eventStore.removeEvents(numberOfEvents); + execute(getPostRequestRunnable(events)); } /** @@ -200,36 +178,33 @@ private void drainBufferAndSend() { * @param buffer the event buffer to be sent * @return the new Runnable object */ - private Runnable getRequestRunnable(final List buffer) { - return new Runnable() { - @Override - public void run() { - if (buffer.size() == 0) { - return; - } + private Runnable getPostRequestRunnable(final List buffer) { + return () -> { + if (buffer.size() == 0) { + return; + } - final SelfDescribingJson post = getFinalPost(buffer); - final int code = httpClientAdapter.post(post); + final SelfDescribingJson post = getFinalPost(buffer); + final int code = httpClientAdapter.post(post); - // Process results - int success = 0; - int failure = 0; - if (!isSuccessfulSend(code)) { - LOGGER.error("BatchEmitter failed to send {} events: code: {}", buffer.size(), code); - failure += buffer.size(); - } else { - LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", buffer.size(), code); - success += buffer.size(); - } + // Process results + int success = 0; + int failure = 0; + if (!isSuccessfulSend(code)) { + LOGGER.error("BatchEmitter failed to send {} events: code: {}", buffer.size(), code); + failure += buffer.size(); + } else { + LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", buffer.size(), code); + success += buffer.size(); + } - // Send the callback if available - if (requestCallback != null) { - if (failure != 0) { - requestCallback.onFailure(success, - buffer.stream().map(te -> te.getEvent()).collect(Collectors.toList())); - } else { - requestCallback.onSuccess(success); - } + // Send the callback if available + if (requestCallback != null) { + if (failure != 0) { + requestCallback.onFailure(success, + buffer.stream().map(TrackerEvent::getEvent).collect(Collectors.toList())); + } else { + requestCallback.onSuccess(success); } } }; @@ -263,7 +238,7 @@ private SelfDescribingJson getFinalPost(final List buffer) { public void close() { isClosing = true; - bufferConsumer.interrupt(); // Kill buffer consumer + checkForEventsToSend.interrupt(); // Kill checkForEventsToSend thread flushBuffer(); // Attempt to send all remaining events //Shutdown executor threadpool diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java new file mode 100644 index 00000000..07a4d4a4 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -0,0 +1,17 @@ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import java.util.Collection; +import java.util.List; + +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; + +public interface EventStore { + + boolean add(TrackerEvent trackerEvent); + + List removeEvents(int numberToRemove); + + int getSize(); + + List getAllEvents(); +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java new file mode 100644 index 00000000..6fb3362e --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -0,0 +1,36 @@ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; + +import java.util.ArrayList; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.List; + +public class InMemoryEventStore implements EventStore { + public final BlockingQueue eventBuffer = new LinkedBlockingQueue<>(); + + @Override + public boolean add(TrackerEvent trackerEvent) { + return eventBuffer.offer(trackerEvent); + } + + @Override + public List removeEvents(int numberToRemove) { + // if numberToRemove is greater than the number of events present, + // it will return all the events (there's no error) + List eventsList = new ArrayList<>(); + eventBuffer.drainTo(eventsList, numberToRemove); + return eventsList; + } + + @Override + public int getSize() { + return eventBuffer.size(); + } + + @Override + public List getAllEvents() { + return new ArrayList<>(eventBuffer); + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 8b7a46df..af7b50fc 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -59,7 +59,7 @@ protected SimpleEmitter(final Builder builder) { */ @Override public void emit(final TrackerEvent event) { - execute(getRequestRunnable(event)); + execute(getGetRequestRunnable(event)); } /** @@ -77,7 +77,7 @@ public void flushBuffer() { * @param event the event to be sent * @return the new Callable object */ - private Runnable getRequestRunnable(final TrackerEvent event) { + private Runnable getGetRequestRunnable(final TrackerEvent event) { return new Runnable() { @Override public void run() { diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index cbfb12aa..a3c63ce8 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import com.google.common.collect.Lists; @@ -79,68 +80,50 @@ public void setUp() { @Test public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws InterruptedException { - // Given List events = createEvents(2); - - // When for (TrackerEvent event : events) { emitter.emit(event); } Thread.sleep(500); - // Then - Assert.assertFalse(mockHttpClientAdapter.isGetCalled); - + Assert.assertFalse(mockHttpClientAdapter.isPostCalled); Assert.assertEquals(2, emitter.getBuffer().size()); Assert.assertEquals(events, emitter.getBuffer()); } @Test public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws InterruptedException { - // Given List events = createEvents(10); - - // When for (TrackerEvent event : events) { emitter.emit(event); } Thread.sleep(500); - // Then Assert.assertTrue(mockHttpClientAdapter.isPostCalled); - @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); assertPayload(events, capturedPayload); - Assert.assertEquals(0, emitter.getBuffer().size()); } @Test public void flushBuffer_shouldEmptyBuffer() throws InterruptedException { - // Given List events = createEvents(2); - - // When for (TrackerEvent event : events) { emitter.emit(event); } - emitter.flushBuffer(); Thread.sleep(500); - // Then Assert.assertTrue(mockHttpClientAdapter.isPostCalled); - @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); assertPayload(events, capturedPayload); - Assert.assertEquals(0, emitter.getBuffer().size()); } @@ -151,20 +134,31 @@ public void setBufferSize_WithNegativeValue_ThrowInvalidArgumentException() { } @Test - public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { - // Given - List events = createEvents(10); + public void setAndGetBufferSizeWorksAsExpected() throws InterruptedException { + emitter.setBufferSize(2); + Assert.assertEquals(2, emitter.getBufferSize()); - // When + List events = createEvents(2); for (TrackerEvent event : events) { emitter.emit(event); } Thread.sleep(500); - // Then Assert.assertTrue(mockHttpClientAdapter.isPostCalled); + Assert.assertEquals(0, emitter.getBuffer().size()); + } + @Test + public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { + List events = createEvents(10); + for (TrackerEvent event : events) { + emitter.emit(event); + } + + Thread.sleep(500); + + Assert.assertTrue(mockHttpClientAdapter.isPostCalled); @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); @@ -189,7 +183,7 @@ public void run() {} @Test public void threadsHaveExpectedNames() { - // A BufferConsumer thread is created on BatchEmitter instantiation. + // A checkForEventsToSend thread is created on BatchEmitter instantiation. // Calling flushBuffer() here to require another thread - causing // creation of a request thread within the scheduledThreadPool. emitter.flushBuffer(); @@ -201,10 +195,32 @@ public void threadsHaveExpectedNames() { threadNames.add(thread.getName()); } - Assert.assertTrue(threadNames.contains("snowplow-emitter-BufferConsumer-thread-1")); + Assert.assertTrue(threadNames.contains("snowplow-emitter-checkForEvents-thread-1")); Assert.assertTrue(threadNames.contains("snowplow-emitter-pool-1-request-thread-1")); } + @Test + public void close_sendsEventsAndStopsThreads() throws InterruptedException { + List events = createEvents(2); + for (TrackerEvent event : events) { + emitter.emit(event); + } + emitter.close(); + + Thread.sleep(500); + + // close() calls flushBuffer() to send all remaining stored events + Assert.assertTrue(mockHttpClientAdapter.isPostCalled); + Assert.assertEquals(0, emitter.getBuffer().size()); + + // these events can be added to storage but should not be sent + List moreEvents = createEvents(20); + for (TrackerEvent event : moreEvents) { + emitter.emit(event); + } + Assert.assertEquals(20, emitter.getBuffer().size()); + } + private List createEvents(int numEvents) { final List payloads = Lists.newArrayList(); for (int i = 0; i < numEvents; i++) { @@ -235,7 +251,7 @@ private void assertPayload(List events, List> boolean matchFound = false; for (Map eventMap : eventPayloads) { //Find the matching events - if (capturedMap.get("eid") == eventMap.get("eid")) { + if (Objects.equals(capturedMap.get("eid"), eventMap.get("eid"))) { matchFound = true; //Assert that all the entries in the event are in the captured payload diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java new file mode 100644 index 00000000..de8162b7 --- /dev/null +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import com.snowplowanalytics.snowplow.tracker.DevicePlatform; +import com.snowplowanalytics.snowplow.tracker.events.PageView; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class InMemoryEventStoreTest { + + private TrackerEvent trackerEvent; + private InMemoryEventStore eventStore; + private List singleEventList; + private List twoEventsList; + + + @Before + public void setUp() { + trackerEvent = createEvent(); + eventStore = new InMemoryEventStore(); + singleEventList = new ArrayList<>(); + twoEventsList = new ArrayList<>(); + + singleEventList.add(trackerEvent); + twoEventsList.add(trackerEvent); + twoEventsList.add(trackerEvent); + } + + @Test + public void correctlyAddAnEventToStore() { + boolean result = eventStore.add(trackerEvent); + + Assert.assertTrue(result); + } + + @Test + public void getSize_returnsCorrectNumberOfStoredEvents() { + storeTwoEvents(); + + Assert.assertEquals(2, eventStore.getSize()); + } + + @Test + public void removeAddedEvent() { + storeTwoEvents(); + + List removedEventList = eventStore.removeEvents(1); + Assert.assertEquals(singleEventList, removedEventList); + Assert.assertEquals(1, eventStore.getSize()); + } + + @Test + public void removeAllEventsIfAskedForMoreEventsThanAreStored() { + storeTwoEvents(); + + List removedEventList = eventStore.removeEvents(100); + Assert.assertEquals(twoEventsList, removedEventList); + Assert.assertEquals(0, eventStore.getSize()); + } + + @Test + public void getAllEvents_doesNotRemoveEventsFromStore() { + storeTwoEvents(); + + List retrievedEventsList = eventStore.getAllEvents(); + Assert.assertEquals(twoEventsList, retrievedEventsList); + Assert.assertEquals(2, eventStore.getSize()); + } + + private TrackerEvent createEvent() { + PageView pv = PageView.builder() + .pageUrl("https://www.snowplowanalytics.com/") + .pageTitle("Snowplow") + .referrer("https://www.google.com/") + .build(); + + return new TrackerEvent(pv, new TrackerParameters("appId", DevicePlatform.ServerSideApp, "namespace", "0.0.0", false), null); + } + + private void storeTwoEvents() { + for (TrackerEvent event : twoEventsList) { + eventStore.add(event); + } + } +} From 813203b72408b2246780fcade0d1273944e257c1 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Tue, 18 Jan 2022 13:07:10 +0000 Subject: [PATCH 071/128] Refactor TrackerEvents for event payload creation (close #291) * Start changing Tracker to create TrackerPayloads not TrackerEvents * Copy payload creation methods into Tracker * Refactor Emitter to use TrackerPayload * Remove TrackerEvent * Remove request callbacks * Use threadpool inside Tracker * Add comment about event ID and dtm timestamp --- .../main/java/com/snowplowanalytics/Main.java | 14 -- .../snowplow/tracker/Tracker.java | 188 +++++++++++++++++- .../tracker/emitter/AbstractEmitter.java | 30 +-- .../tracker/emitter/BatchEmitter.java | 51 ++--- .../snowplow/tracker/emitter/Emitter.java | 14 +- .../snowplow/tracker/emitter/EventStore.java | 9 +- .../tracker/emitter/InMemoryEventStore.java | 14 +- .../tracker/emitter/RequestCallback.java | 41 ---- .../tracker/emitter/SimpleEmitter.java | 52 ++--- .../tracker/payload/TrackerEvent.java | 164 --------------- .../tracker/payload/TrackerParameters.java | 5 - .../snowplow/tracker/TrackerTest.java | 111 ++++++++--- .../tracker/emitter/BatchEmitterTest.java | 93 +++++---- .../emitter/InMemoryEventStoreTest.java | 44 ++-- 14 files changed, 386 insertions(+), 444 deletions(-) delete mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java delete mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 6b9f2ab3..037bb9d5 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -17,7 +17,6 @@ import com.snowplowanalytics.snowplow.tracker.Tracker; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; -import com.snowplowanalytics.snowplow.tracker.emitter.RequestCallback; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; @@ -53,19 +52,6 @@ public static void main(String[] args) { // build an emitter, this is used by the tracker to batch and schedule transmission of events BatchEmitter emitter = BatchEmitter.builder() .url(collectorEndpoint) - .requestCallback(new RequestCallback() { - // let us know on successes (may be called multiple times) - @Override - public synchronized void onSuccess(int successCount) { - System.out.println("Successfully sent " + successCount + " events"); - } - - // let us know if something has gone wrong (may be called multiple times) - @Override - public synchronized void onFailure(int successCount, List failedEvents) { - System.err.println("Successfully sent " + successCount + " events; failed to send " + failedEvents.size() + " events"); - } - }) .bufferSize(4) // send an event every time one is given (no batching). In production this number should be higher, depending on the size/event volume .build(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index a992c48c..67e91d28 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -14,16 +14,26 @@ import com.google.common.base.Preconditions; +import com.snowplowanalytics.snowplow.tracker.constants.Constants; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; + +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; public class Tracker { private Emitter emitter; private Subject subject; private final TrackerParameters parameters; + protected ExecutorService executor; /** * Creates a new Snowplow Tracker. @@ -42,6 +52,12 @@ private Tracker(TrackerBuilder builder) { this.parameters = new TrackerParameters(builder.appId, builder.platform, builder.namespace, Version.TRACKER, builder.base64Encoded); this.emitter = builder.emitter; this.subject = builder.subject; + + if (builder.requestExecutorService != null) { + this.executor = builder.requestExecutorService; + } else { + this.executor = Executors.newScheduledThreadPool(builder.threadCount, new TrackerThreadFactory()); + } } /** @@ -55,6 +71,8 @@ public static class TrackerBuilder { private Subject subject = null; // Optional private DevicePlatform platform = DevicePlatform.ServerSideApp; // Optional private boolean base64Encoded = true; // Optional + private int threadCount = 50; // Optional + private ExecutorService requestExecutorService = null; // Optional /** * @param emitter Emitter to which events will be sent @@ -94,6 +112,30 @@ public TrackerBuilder base64(Boolean base64) { return this; } + /** + * Sets the Thread Count for the ExecutorService + * + * @param threadCount the size of the thread pool + * @return itself + */ + public TrackerBuilder threadCount(final int threadCount) { + this.threadCount = threadCount; + return this; + } + + /** + * Set a custom ExecutorService to send http request. + * + * @param executorService the ExecutorService to use + * @return itself + */ + public TrackerBuilder requestExecutorService(final ExecutorService executorService) { + this.requestExecutorService = executorService; + return this; + } + + + /** * Creates a new Tracker * @@ -183,6 +225,45 @@ public TrackerParameters getParameters() { // --- Event Tracking Functions + /** + * Sends a runnable to the executor service. + * + * @param runnable the runnable to be queued + */ + protected void execute(final Runnable runnable) { + this.executor.execute(runnable); + } + + /** + * Copied from `Executors.defaultThreadFactory()`. + * The only change is the generated name prefix. + */ + static class TrackerThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + TrackerThreadFactory() { + SecurityManager securityManager = System.getSecurityManager(); + this.group = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); + this.namePrefix = "snowplow-tracker-pool-" + poolNumber.getAndIncrement() + "-event-thread-"; + } + + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(this.group, runnable, this.namePrefix + this.threadNumber.getAndIncrement(), 0L); + if (thread.isDaemon()) { + thread.setDaemon(false); + } + + if (thread.getPriority() != 5) { + thread.setPriority(5); + } + + return thread; + } + } + /** * Handles tracking the different types of events that * the Tracker can encounter. @@ -190,7 +271,108 @@ public TrackerParameters getParameters() { * @param event the event to track */ public void track(Event event) { - // Emit the event - this.emitter.emit(new TrackerEvent(event, this.parameters, this.subject)); + execute(getProcessEventRunnable(event)); + } + + private Runnable getProcessEventRunnable(Event event) { + return () -> { + // a list because Ecommerce events become multiple Payloads + List processedEvents = eventTypeSpecificPreProcessing(event); + for (Event processedEvent : processedEvents) { + // Event ID (eid) and device_created_timestamp (dtm) are added during getPayload() + TrackerPayload payload = (TrackerPayload) processedEvent.getPayload(); + + addTrackerParameters(payload); + addContext(processedEvent, payload); + addSubject(processedEvent, payload); + this.emitter.add(payload); + } + }; + } + + private List eventTypeSpecificPreProcessing(Event event) { + // Different event types must be processed in slightly different ways. + // EcommerceTransaction events are an outlier, as they are processed into + // multiple payloads (a "tr" event plus one "ti" event per item). + // Because of this, this method returns a list of Events. + List eventList = new ArrayList<>(); + final Class eventClass = event.getClass(); + + if (eventClass.equals(Unstructured.class)) { + // Need to set the Base64 rule for Unstructured events + final Unstructured unstructured = (Unstructured) event; + unstructured.setBase64Encode(this.parameters.getBase64Encoded()); + eventList.add(unstructured); + + } else if (eventClass.equals(EcommerceTransaction.class)) { + final EcommerceTransaction ecommerceTransaction = (EcommerceTransaction) event; + eventList.add(ecommerceTransaction); + + // Track each item individually + for (final EcommerceTransactionItem item : ecommerceTransaction.getItems()) { + item.setDeviceCreatedTimestamp(ecommerceTransaction.getDeviceCreatedTimestamp()); + eventList.add(item); + } + } else if (eventClass.equals(Timing.class) || eventClass.equals(ScreenView.class)) { + // Timing and ScreenView events are wrapper classes for Unstructured events + // Need to create Unstructured events from them to send. + final Unstructured unstructured = Unstructured.builder() + .eventData((SelfDescribingJson) event.getPayload()) + .customContext(event.getContext()) + .deviceCreatedTimestamp(event.getDeviceCreatedTimestamp()) + .trueTimestamp(event.getTrueTimestamp()) + .eventId(event.getEventId()) + .subject(event.getSubject()) + .build(); + + unstructured.setBase64Encode(this.parameters.getBase64Encoded()); + eventList.add(unstructured); + + } else { + eventList.add(event); + } + return eventList; + } + + private void addTrackerParameters(TrackerPayload payload) { + payload.add(Parameter.PLATFORM, this.parameters.getPlatform().toString()); + payload.add(Parameter.APP_ID, this.parameters.getAppId()); + payload.add(Parameter.NAMESPACE, this.parameters.getNamespace()); + payload.add(Parameter.TRACKER_VERSION, this.parameters.getTrackerVersion()); + } + + private void addContext(Event event, TrackerPayload payload) { + List entities = event.getContext(); + + // Build the final context and add it to the payload + if (entities != null && entities.size() > 0) { + SelfDescribingJson envelope = getFinalContext(entities); + payload.addMap(envelope.getMap(), this.parameters.getBase64Encoded(), Parameter.CONTEXT_ENCODED, Parameter.CONTEXT); + } + } + + /** + * Builds the final event context. + * + * @param entities the base event context + * @return the final event context json with many entities inside + */ + private SelfDescribingJson getFinalContext(List entities) { + List> entityMaps = new LinkedList<>(); + for (SelfDescribingJson selfDescribingJson : entities) { + entityMaps.add(selfDescribingJson.getMap()); + } + return new SelfDescribingJson(Constants.SCHEMA_CONTEXTS, entityMaps); + } + + private void addSubject(Event event, TrackerPayload payload) { + Subject eventSubject = event.getSubject(); + + // Add subject if available + if (eventSubject != null) { + payload.addMap(new HashMap<>(eventSubject.getSubject())); + } else if (this.subject != null) { + payload.addMap(new HashMap<>(this.subject.getSubject())); + } } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index b1108a4d..6b3075a1 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -22,8 +22,8 @@ import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import okhttp3.OkHttpClient; /** @@ -33,13 +33,11 @@ public abstract class AbstractEmitter implements Emitter { protected HttpClientAdapter httpClientAdapter; - protected RequestCallback requestCallback; protected ExecutorService executor; public static abstract class Builder> { private HttpClientAdapter httpClientAdapter; // Optional - private RequestCallback requestCallback = null; // Optional private int threadCount = 50; // Optional private ExecutorService requestExecutorService = null; // Optional private String collectorUrl = null; // Required if not specifying a httpClientAdapter @@ -68,18 +66,6 @@ public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { return self(); } - /** - * An optional Request Callback for adding the ability to handle failure cases - * for sending. - * - * @param requestCallback the emitter request callback - * @return itself - */ - public T requestCallback(final RequestCallback requestCallback) { - this.requestCallback = requestCallback; - return self(); - } - /** * Sets the Thread Count for the ExecutorService * @@ -132,8 +118,6 @@ protected AbstractEmitter(final Builder builder) { .build(); } - this.requestCallback = builder.requestCallback; - if (builder.requestExecutorService != null) { this.executor = builder.requestExecutorService; } else { @@ -142,12 +126,12 @@ protected AbstractEmitter(final Builder builder) { } /** - * Adds an event to the buffer + * Adds a payload to the buffer * - * @param event an event + * @param payload an payload */ @Override - public abstract void emit(TrackerEvent event); + public abstract void add(TrackerPayload payload); /** * Customize the emitter buffer size to any valid integer greater than zero. @@ -159,7 +143,7 @@ protected AbstractEmitter(final Builder builder) { public abstract void setBufferSize(final int bufferSize); /** - * Removes all events from the buffer and sends them + * Removes all payloads from the buffer and sends them */ @Override public abstract void flushBuffer(); @@ -173,12 +157,12 @@ protected AbstractEmitter(final Builder builder) { public abstract int getBufferSize(); /** - * Returns List of Events that are in the buffer. + * Returns List of Payloads that are in the buffer. * * @return the buffered events */ @Override - public abstract List getBuffer(); + public abstract List getBuffer(); /** * Sends a runnable to the executor service. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 377c1e8d..a4b0d11d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -17,14 +17,12 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import org.slf4j.Logger; @@ -32,7 +30,7 @@ /** * An emitter that emit a batch of events in a single call - * It uses the post method of under-laying http adapter + * It uses the post method of underlying http adapter */ public class BatchEmitter extends AbstractEmitter implements Closeable { @@ -100,21 +98,21 @@ protected BatchEmitter(final Builder builder) { } /** - * Adds a TrackerEvent to the concurrent queue buffer + * Adds a TrackerPayload to the concurrent queue buffer * - * @param event an event + * @param payload a payload */ @Override - public void emit(final TrackerEvent event) { - boolean result = eventStore.add(event); + public void add(final TrackerPayload payload) { + boolean result = eventStore.add(payload); if (!result) { - LOGGER.error("Unable to add event to emitter, emitter buffer is full"); + LOGGER.error("Unable to add payload to emitter, emitter buffer is full"); } } /* - * Forces all the events currently in the buffer to be sent + * Forces all the payloads currently in the buffer to be sent */ @Override public void flushBuffer() { @@ -122,12 +120,12 @@ public void flushBuffer() { } /** - * Returns List of Events that are in the buffer. + * Returns List of Payloads that are in the buffer. * * @return the buffered events */ @Override - public List getBuffer() { + public List getBuffer() { return eventStore.getAllEvents(); } @@ -168,8 +166,8 @@ private Runnable getCheckForEventsToSendRunnable() { } private void drainEventsAndSend(int numberOfEvents) { - List events = eventStore.removeEvents(numberOfEvents); - execute(getPostRequestRunnable(events)); + List payloads = eventStore.removeEvents(numberOfEvents); + execute(getPostRequestRunnable(payloads)); } /** @@ -178,7 +176,7 @@ private void drainEventsAndSend(int numberOfEvents) { * @param buffer the event buffer to be sent * @return the new Runnable object */ - private Runnable getPostRequestRunnable(final List buffer) { + private Runnable getPostRequestRunnable(final List buffer) { return () -> { if (buffer.size() == 0) { return; @@ -188,24 +186,10 @@ private Runnable getPostRequestRunnable(final List buffer) { final int code = httpClientAdapter.post(post); // Process results - int success = 0; - int failure = 0; if (!isSuccessfulSend(code)) { LOGGER.error("BatchEmitter failed to send {} events: code: {}", buffer.size(), code); - failure += buffer.size(); } else { LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", buffer.size(), code); - success += buffer.size(); - } - - // Send the callback if available - if (requestCallback != null) { - if (failure != 0) { - requestCallback.onFailure(success, - buffer.stream().map(TrackerEvent::getEvent).collect(Collectors.toList())); - } else { - requestCallback.onSuccess(success); - } } }; } @@ -216,16 +200,13 @@ private Runnable getPostRequestRunnable(final List buffer) { * @param buffer the event buffer * @return the constructed POST payload */ - private SelfDescribingJson getFinalPost(final List buffer) { + private SelfDescribingJson getFinalPost(final List buffer) { final List> toSendPayloads = new ArrayList<>(); final String sentTimestamp = Long.toString(System.currentTimeMillis()); - for (TrackerEvent event : buffer) { - List payloads = event.getTrackerPayloads(); - for (TrackerPayload payload : payloads) { - payload.add(Parameter.DEVICE_SENT_TIMESTAMP, sentTimestamp); - toSendPayloads.add(payload.getMap()); - } + for (TrackerPayload payload : buffer) { + payload.add(Parameter.DEVICE_SENT_TIMESTAMP, sentTimestamp); + toSendPayloads.add(payload.getMap()); } return new SelfDescribingJson(Constants.SCHEMA_PAYLOAD_DATA, toSendPayloads); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index c51a456d..fddc0c56 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -14,7 +14,7 @@ import java.util.List; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; /** * Emitter interface. @@ -22,17 +22,17 @@ public interface Emitter { /** - * Adds an event to the buffer and checks whether + * Adds a payload to the buffer and checks whether * we have reached the buffer limit yet. * - * @param event an event to be emitted + * @param payload a payload to be emitted */ - void emit(TrackerEvent event); + void add(TrackerPayload payload); /** * Customize the emitter buffer size to any valid integer * greater than zero. - * - Will only effect the BatchEmitter + * - Will only affect the BatchEmitter * * @param bufferSize number of events to collect before * sending @@ -56,9 +56,9 @@ public interface Emitter { int getBufferSize(); /** - * Returns the List of Events that are in the buffer. + * Returns the List of Payloads that are in the buffer. * * @return the buffer events */ - List getBuffer(); + List getBuffer(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index 07a4d4a4..4f61e1d3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -1,17 +1,16 @@ package com.snowplowanalytics.snowplow.tracker.emitter; -import java.util.Collection; import java.util.List; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; public interface EventStore { - boolean add(TrackerEvent trackerEvent); + boolean add(TrackerPayload trackerPayload); - List removeEvents(int numberToRemove); + List removeEvents(int numberToRemove); int getSize(); - List getAllEvents(); + List getAllEvents(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index 6fb3362e..e3ab9477 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -1,6 +1,6 @@ package com.snowplowanalytics.snowplow.tracker.emitter; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import java.util.ArrayList; import java.util.concurrent.BlockingQueue; @@ -8,18 +8,18 @@ import java.util.List; public class InMemoryEventStore implements EventStore { - public final BlockingQueue eventBuffer = new LinkedBlockingQueue<>(); + public final BlockingQueue eventBuffer = new LinkedBlockingQueue<>(); @Override - public boolean add(TrackerEvent trackerEvent) { - return eventBuffer.offer(trackerEvent); + public boolean add(TrackerPayload trackerPayload) { + return eventBuffer.offer(trackerPayload); } @Override - public List removeEvents(int numberToRemove) { + public List removeEvents(int numberToRemove) { // if numberToRemove is greater than the number of events present, // it will return all the events (there's no error) - List eventsList = new ArrayList<>(); + List eventsList = new ArrayList<>(); eventBuffer.drainTo(eventsList, numberToRemove); return eventsList; } @@ -30,7 +30,7 @@ public int getSize() { } @Override - public List getAllEvents() { + public List getAllEvents() { return new ArrayList<>(eventBuffer); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java deleted file mode 100644 index 4df7c8bb..00000000 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/RequestCallback.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package com.snowplowanalytics.snowplow.tracker.emitter; - -import java.util.List; - -import com.snowplowanalytics.snowplow.tracker.events.Event; - -/** - * Provides a callback interface for reporting counts of successfully sent - * events and returning any failed events to be handled by the developer. - */ -public interface RequestCallback { - - /** - * If all events are sent successfully then the count - * of sent events are returned. - * - * @param successCount the successful count - */ - void onSuccess(int successCount); - - /** - * If all/some events failed then the count of successful - * events is returned along with all the failed Events. - * - * @param successCount the successful count - * @param failedEvents the list of failed events - */ - void onFailure(int successCount, List failedEvents); -} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index af7b50fc..e3849efb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -18,10 +18,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; -import com.snowplowanalytics.snowplow.tracker.events.Event; /** * An emitter which sends events as soon as they are received via @@ -52,14 +50,9 @@ protected SimpleEmitter(final Builder builder) { super(builder); } - /** - * Adds an event to the buffer and instantly sends it - * - * @param event an event - */ @Override - public void emit(final TrackerEvent event) { - execute(getGetRequestRunnable(event)); + public void add(TrackerPayload payload) { + // nothing happens } /** @@ -74,42 +67,23 @@ public void flushBuffer() { /** * Returns a Runnable GET Request operation * - * @param event the event to be sent + * @param payload the event to be sent * @return the new Callable object */ - private Runnable getGetRequestRunnable(final TrackerEvent event) { + private Runnable getGetRequestRunnable(final TrackerPayload payload) { return new Runnable() { @Override public void run() { - int success = 0; - int failure = 0; - - List payloads = event.getTrackerPayloads(); - - for (TrackerPayload payload : payloads) { - payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); - final int code = httpClientAdapter.get(payload); - - // Process results - if (!isSuccessfulSend(code)) { - LOGGER.error("SimpleEmitter failed to send {} events: code: {}", 1, code); - failure += 1; - } else { - LOGGER.debug("SimpleEmitter successfully sent {} events: code: {}", 1, code); - success += 1; - } + payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); + final int code = httpClientAdapter.get(payload); + + // Process results + if (!isSuccessfulSend(code)) { + LOGGER.error("SimpleEmitter failed to send {} events: code: {}", 1, code); + } else { + LOGGER.debug("SimpleEmitter successfully sent {} events: code: {}", 1, code); } - // Send the callback if available - if (requestCallback != null) { - if (failure != 0) { - final List buffer = new ArrayList<>(); - buffer.add(event.getEvent()); - requestCallback.onFailure(success, buffer); - } else { - requestCallback.onSuccess(success); - } - } } }; } @@ -121,7 +95,7 @@ public void run() { * @return the empty buffer */ @Override - public List getBuffer() { + public List getBuffer() { return new ArrayList<>(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java deleted file mode 100644 index ced65783..00000000 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerEvent.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package com.snowplowanalytics.snowplow.tracker.payload; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import com.snowplowanalytics.snowplow.tracker.Subject; -import com.snowplowanalytics.snowplow.tracker.constants.Constants; -import com.snowplowanalytics.snowplow.tracker.constants.Parameter; -import com.snowplowanalytics.snowplow.tracker.events.*; - -/** - * A TrackerEvent which allows the TrackerPayload to be filled later. The payload will be - * filled by the Emitter in the Emitter thread, using the getTrackerPayload() method. - */ -public class TrackerEvent { - - private final Event event; - private final TrackerParameters parameters; - private final Subject subject; - - public TrackerEvent(final Event event, final TrackerParameters parameters, final Subject subject) { - this.event = event; - this.parameters = parameters; - this.subject = subject; - } - - /** - * Returns the {@link Event} - * - * @return The {@link Event} - */ - public Event getEvent() { - return this.event; - } - - /** - * Converts a {@link Event} to a list of {@link TrackerPayload} and caches the values. - * Returns a list as some Events contain nested payloads (e.g. {@link EcommerceTransaction}) - * Adds fields to the {@link TrackerPayload} based on the type of the {@link Event}. - * - * @return The populated TrackerPayloads - */ - public List getTrackerPayloads() { - final List payloads = new ArrayList<>(); - final List contexts = event.getContext(); - final Subject subject = event.getSubject(); - - // Figure out what type of event it is - final Class eventClass = event.getClass(); - - if (eventClass.equals(Unstructured.class)) { - - // Need to set the Base64 rule for Unstructured events - final Unstructured unstructured = (Unstructured) event; - unstructured.setBase64Encode(this.parameters.getBase64Encoded()); - TrackerPayload payload = unstructured.getPayload(); - addTrackerParameters(payload); - addContextsAndSubject(contexts, subject, payload); - payloads.add(payload); - } else if (eventClass.equals(Timing.class) || eventClass.equals(ScreenView.class)) { - - // These are wrapper classes for Unstructured events; need to create - // Unstructured events from them and resend. - final Unstructured unstructured = Unstructured.builder() - .eventData((SelfDescribingJson) event.getPayload()) - .customContext(contexts) - .deviceCreatedTimestamp(event.getDeviceCreatedTimestamp()) - .trueTimestamp(event.getTrueTimestamp()) - .eventId(event.getEventId()) - .subject(subject) - .build(); - - unstructured.setBase64Encode(this.parameters.getBase64Encoded()); - TrackerPayload payload = unstructured.getPayload(); - addTrackerParameters(payload); - addContextsAndSubject(contexts, subject, payload); - payloads.add(payload); - } else if (eventClass.equals(EcommerceTransaction.class)) { - - final EcommerceTransaction ecommerceTransaction = (EcommerceTransaction) event; - TrackerPayload payload = ecommerceTransaction.getPayload(); - addTrackerParameters(payload); - addContextsAndSubject(contexts, subject, payload); - payloads.add(payload); - - // Track each item individually - for (final EcommerceTransactionItem item : ecommerceTransaction.getItems()) { - - item.setDeviceCreatedTimestamp(ecommerceTransaction.getDeviceCreatedTimestamp()); - TrackerPayload itemPayload = item.getPayload(); - addTrackerParameters(itemPayload); - addContextsAndSubject(item.getContext(), item.getSubject(), itemPayload); - payloads.add(itemPayload); - } - } else { - - // For all other events, simply get the payload - TrackerPayload payload = (TrackerPayload) event.getPayload(); - addTrackerParameters(payload); - addContextsAndSubject(contexts, subject, payload); - payloads.add(payload); - } - - return payloads; - } - - /** - * Adds the context and subject to the event payload - * - * @param contexts the base event context - can be null or empty - * @param subject the event subject - can be null - * @param payload the payload to add the contexts and subjects to - */ - private void addContextsAndSubject(final List contexts, final Subject subject, TrackerPayload payload) { - // Build the final context and add it to the payload - if (contexts != null && contexts.size() > 0) { - SelfDescribingJson envelope = getFinalContext(contexts); - payload.addMap(envelope.getMap(), this.parameters.getBase64Encoded(), Parameter.CONTEXT_ENCODED, Parameter.CONTEXT); - } - - // Add subject if available - if (subject != null) { - payload.addMap(new HashMap<>(subject.getSubject())); - } else if (this.subject != null) { - payload.addMap(new HashMap<>(this.subject.getSubject())); - } - } - - /** - * Builds the final event context. - * - * @param contexts the base event context - * @return the final event context json with many contexts inside - */ - private SelfDescribingJson getFinalContext(List contexts) { - List> contextMaps = new LinkedList<>(); - for (SelfDescribingJson selfDescribingJson : contexts) { - contextMaps.add(selfDescribingJson.getMap()); - } - return new SelfDescribingJson(Constants.SCHEMA_CONTEXTS, contextMaps); - } - - private void addTrackerParameters(TrackerPayload payload) { - payload.add(Parameter.PLATFORM, this.parameters.getPlatform().toString()); - payload.add(Parameter.APP_ID, this.parameters.getAppId()); - payload.add(Parameter.NAMESPACE, this.parameters.getNamespace()); - payload.add(Parameter.TRACKER_VERSION, this.parameters.getTrackerVersion()); - } -} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java index 90bac316..66d5be3e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -14,11 +14,6 @@ import com.snowplowanalytics.snowplow.tracker.DevicePlatform; -/** - * A TrackerEvent which allows the TrackerPayload to be filled later. The - * payload will be filled by the Emitter in the Emitter thread, using the - * getTrackerPayload() method. - */ public class TrackerParameters { private final String trackerVersion; diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 1743c9e3..7e1ce57b 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -15,6 +15,7 @@ import java.util.*; import static java.util.Collections.singletonList; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; @@ -25,7 +26,6 @@ import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; public class TrackerTest { @@ -33,11 +33,11 @@ public class TrackerTest { public static final String EXPECTED_EVENT_ID = "15e9b149-6029-4f6e-8447-5b9797c9e6be"; public static class MockEmitter implements Emitter { - public ArrayList eventList = new ArrayList<>(); + public ArrayList eventList = new ArrayList<>(); @Override - public void emit(TrackerEvent event) { - eventList.add(event); + public void add(TrackerPayload payload) { + eventList.add(payload); } @Override @@ -52,7 +52,7 @@ public int getBufferSize() { } @Override - public List getBuffer() { + public List getBuffer() { return null; } } @@ -75,7 +75,7 @@ public void setUp() { // --- Event Tests @Test - public void testEcommerceEvent() { + public void testEcommerceEvent() throws InterruptedException { // Given EcommerceTransactionItem item = EcommerceTransactionItem.builder() .itemId("order_id") @@ -110,7 +110,9 @@ public void testEcommerceEvent() { .build()); // Then - List results = mockEmitter.eventList.get(0).getTrackerPayloads(); + Thread.sleep(500); + + List results = mockEmitter.eventList; assertEquals(2, results.size()); Map result1 = results.get(0).getMap(); @@ -159,7 +161,7 @@ public void testEcommerceEvent() { } @Test - public void testUnstructuredEventWithContext() { + public void testUnstructuredEventWithContext() throws InterruptedException { // When tracker.track(Unstructured.builder() .eventData(new SelfDescribingJson( @@ -173,7 +175,9 @@ public void testUnstructuredEventWithContext() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -190,7 +194,7 @@ public void testUnstructuredEventWithContext() { } @Test - public void testUnstructuredEventWithoutContext() { + public void testUnstructuredEventWithoutContext() throws InterruptedException { // When tracker.track(Unstructured.builder() .eventData(new SelfDescribingJson( @@ -203,7 +207,9 @@ public void testUnstructuredEventWithoutContext() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -219,7 +225,7 @@ public void testUnstructuredEventWithoutContext() { } @Test - public void testUnstructuredEventWithoutTrueTimestamp() { + public void testUnstructuredEventWithoutTrueTimestamp() throws InterruptedException { // When tracker.track(Unstructured.builder() .eventData(new SelfDescribingJson( @@ -231,7 +237,9 @@ public void testUnstructuredEventWithoutTrueTimestamp() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -246,7 +254,7 @@ public void testUnstructuredEventWithoutTrueTimestamp() { } @Test - public void testTrackPageView() { + public void testTrackPageView() throws InterruptedException { tracker = new Tracker.TrackerBuilder(this.mockEmitter, "AF003", "cloudfront") .subject(new Subject.SubjectBuilder().build()) .base64(false) @@ -265,7 +273,9 @@ public void testTrackPageView() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -284,7 +294,7 @@ public void testTrackPageView() { } @Test - public void testTrackTwoEvents() { + public void testTrackTwoEvents() throws InterruptedException { // When tracker.track(PageView.builder() .pageUrl("url") @@ -295,6 +305,8 @@ public void testTrackTwoEvents() { .eventId("9783090a-dace-4c85-a75c-933b4596a6c5") .build()); + Thread.sleep(500); + tracker.track(PageView.builder() .pageUrl("url") .pageTitle("title") @@ -305,10 +317,12 @@ public void testTrackTwoEvents() { .build()); // Then - List results = mockEmitter.eventList; + Thread.sleep(500); + + List results = mockEmitter.eventList; assertEquals(2, results.size()); - Map result1 = results.get(0).getTrackerPayloads().get(0).getMap(); + Map result1 = results.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -324,7 +338,7 @@ public void testTrackTwoEvents() { .put("url", "url") .build(), result1); - Map result2 = results.get(1).getTrackerPayloads().get(0).getMap(); + Map result2 = results.get(1).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -342,7 +356,7 @@ public void testTrackTwoEvents() { } @Test - public void testTrackScreenView() { + public void testTrackScreenView() throws InterruptedException { // When tracker.track(ScreenView.builder() .name("name") @@ -354,7 +368,9 @@ public void testTrackScreenView() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -371,7 +387,7 @@ public void testTrackScreenView() { } @Test - public void testTrackScreenViewWithTimestamp() { + public void testTrackScreenViewWithTimestamp() throws InterruptedException { // When tracker.track(ScreenView.builder() .name("name") @@ -382,7 +398,9 @@ public void testTrackScreenViewWithTimestamp() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("dtm", "123456") .put("ttm", "456789") @@ -398,7 +416,7 @@ public void testTrackScreenViewWithTimestamp() { } @Test - public void testTrackScreenViewWithDefaultContextAndTimestamp() { + public void testTrackScreenViewWithDefaultContextAndTimestamp() throws InterruptedException { // When tracker.track(ScreenView.builder() .name("name") @@ -410,7 +428,9 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -427,7 +447,7 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() { } @Test - public void testTrackTiming() { + public void testTrackTiming() throws InterruptedException { // When tracker.track(Timing.builder() .category("category") @@ -441,7 +461,9 @@ public void testTrackTiming() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) @@ -458,7 +480,7 @@ public void testTrackTiming() { } @Test - public void testTrackTimingWithSubject() { + public void testTrackTimingWithSubject() throws InterruptedException { // Make Subject Subject s1 = new Subject.SubjectBuilder().build(); s1.setIpAddress("127.0.0.1"); @@ -478,7 +500,9 @@ public void testTrackTimingWithSubject() { .build()); // Then - Map result = mockEmitter.eventList.get(0).getTrackerPayloads().get(0).getMap(); + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); assertEquals(ImmutableMap.builder() .put("p", "srv") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") @@ -500,7 +524,7 @@ public void testTrackTimingWithSubject() { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); - assertEquals("java-0.10.1", tracker.getTrackerVersion()); + assertEquals("java-0.11.0", tracker.getTrackerVersion()); } @Test @@ -546,4 +570,31 @@ public void testSetNamespace() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); assertEquals("namespace", tracker.getNamespace()); } + + @Test + public void threadsHaveExpectedNames() { + // A new thread should be created for each event tracked, + // up to the configurable pool size limit + tracker.track(PageView.builder() + .pageUrl("url") + .pageTitle("title") + .referrer("referer") + .build()); + + tracker.track(PageView.builder() + .pageUrl("url") + .pageTitle("title") + .referrer("referer") + .build()); + + // Create a list of all live thread names + List threadList = new ArrayList<>(Thread.getAllStackTraces().keySet()); + List threadNames = new ArrayList<>(); + for (Thread thread : threadList) { + threadNames.add(thread.getName()); + } + + Assert.assertTrue(threadNames.contains("snowplow-tracker-pool-1-event-thread-1")); + Assert.assertTrue(threadNames.contains("snowplow-tracker-pool-1-event-thread-2")); + } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index a3c63ce8..47d69843 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -19,19 +19,16 @@ import com.google.common.collect.Lists; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import com.snowplowanalytics.snowplow.tracker.DevicePlatform; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.events.PageView; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; @@ -80,23 +77,23 @@ public void setUp() { @Test public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws InterruptedException { - List events = createEvents(2); - for (TrackerEvent event : events) { - emitter.emit(event); + List payloads = createPayloads(2); + for (TrackerPayload payload : payloads) { + emitter.add(payload); } Thread.sleep(500); Assert.assertFalse(mockHttpClientAdapter.isPostCalled); Assert.assertEquals(2, emitter.getBuffer().size()); - Assert.assertEquals(events, emitter.getBuffer()); + Assert.assertEquals(payloads, emitter.getBuffer()); } @Test public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws InterruptedException { - List events = createEvents(10); - for (TrackerEvent event : events) { - emitter.emit(event); + List payloads = createPayloads(10); + for (TrackerPayload payload : payloads) { + emitter.add(payload); } Thread.sleep(500); @@ -105,15 +102,15 @@ public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Interrupte @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); - assertPayload(events, capturedPayload); + assertPayload(payloads, capturedPayload); Assert.assertEquals(0, emitter.getBuffer().size()); } @Test public void flushBuffer_shouldEmptyBuffer() throws InterruptedException { - List events = createEvents(2); - for (TrackerEvent event : events) { - emitter.emit(event); + List payloads = createPayloads(2); + for (TrackerPayload payload : payloads) { + emitter.add(payload); } emitter.flushBuffer(); @@ -123,7 +120,7 @@ public void flushBuffer_shouldEmptyBuffer() throws InterruptedException { @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); - assertPayload(events, capturedPayload); + assertPayload(payloads, capturedPayload); Assert.assertEquals(0, emitter.getBuffer().size()); } @@ -138,9 +135,9 @@ public void setAndGetBufferSizeWorksAsExpected() throws InterruptedException { emitter.setBufferSize(2); Assert.assertEquals(2, emitter.getBufferSize()); - List events = createEvents(2); - for (TrackerEvent event : events) { - emitter.emit(event); + List payloads = createPayloads(2); + for (TrackerPayload payload : payloads) { + emitter.add(payload); } Thread.sleep(500); @@ -151,9 +148,9 @@ public void setAndGetBufferSizeWorksAsExpected() throws InterruptedException { @Test public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { - List events = createEvents(10); - for (TrackerEvent event : events) { - emitter.emit(event); + List payloads = createPayloads(10); + for (TrackerPayload payload : payloads) { + emitter.add(payload); } Thread.sleep(500); @@ -161,7 +158,7 @@ public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { Assert.assertTrue(mockHttpClientAdapter.isPostCalled); @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); - + for (Map payloadMap : capturedPayload) { Assert.assertTrue(payloadMap.containsKey(Parameter.DEVICE_SENT_TIMESTAMP)); } @@ -201,9 +198,9 @@ public void threadsHaveExpectedNames() { @Test public void close_sendsEventsAndStopsThreads() throws InterruptedException { - List events = createEvents(2); - for (TrackerEvent event : events) { - emitter.emit(event); + List payloads = createPayloads(2); + for (TrackerPayload payload : payloads) { + emitter.add(payload); } emitter.close(); @@ -214,36 +211,35 @@ public void close_sendsEventsAndStopsThreads() throws InterruptedException { Assert.assertEquals(0, emitter.getBuffer().size()); // these events can be added to storage but should not be sent - List moreEvents = createEvents(20); - for (TrackerEvent event : moreEvents) { - emitter.emit(event); + List morePayloads = createPayloads(20); + for (TrackerPayload payload : morePayloads) { + emitter.add(payload); } Assert.assertEquals(20, emitter.getBuffer().size()); } - private List createEvents(int numEvents) { - final List payloads = Lists.newArrayList(); - for (int i = 0; i < numEvents; i++) { - payloads.add(createEvent()); - } - return payloads; - } - - private TrackerEvent createEvent() { + private TrackerPayload createPayload() { PageView pv = PageView.builder() - .pageUrl("https://www.snowplowanalytics.com/") - .pageTitle("Snowplow") - .referrer("https://www.google.com/") - .build(); + .pageUrl("https://www.snowplowanalytics.com/") + .pageTitle("Snowplow") + .referrer("https://www.google.com/") + .build(); + + return pv.getPayload(); + } - return new TrackerEvent(pv, new TrackerParameters("appId", DevicePlatform.ServerSideApp, "namespace", "0.0.0", false), null); + private List createPayloads(int numPayloads) { + final List payloads = Lists.newArrayList(); + for (int i = 0; i < numPayloads; i++) { + payloads.add(createPayload()); + } + return payloads; } - private void assertPayload(List events, List> capturedPayload) { + private void assertPayload(List payloads, List> capturedPayload) { List> eventPayloads = new ArrayList<>(); - for (TrackerEvent event : events) { - //All PageView events so we can get(0) from payloads - eventPayloads.add(event.getTrackerPayloads().get(0).getMap()); + for (TrackerPayload payload : payloads) { + eventPayloads.add(payload.getMap()); } //Iterate through all captured payloads @@ -264,3 +260,4 @@ private void assertPayload(List events, List> } } } + diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java index de8162b7..7c341dfb 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -12,10 +12,8 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -import com.snowplowanalytics.snowplow.tracker.DevicePlatform; import com.snowplowanalytics.snowplow.tracker.events.PageView; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerEvent; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -25,78 +23,78 @@ public class InMemoryEventStoreTest { - private TrackerEvent trackerEvent; + private TrackerPayload trackerPayload; private InMemoryEventStore eventStore; - private List singleEventList; - private List twoEventsList; + private List singleEventList; + private List twoEventsList; @Before public void setUp() { - trackerEvent = createEvent(); + trackerPayload = createPayload(); eventStore = new InMemoryEventStore(); singleEventList = new ArrayList<>(); twoEventsList = new ArrayList<>(); - singleEventList.add(trackerEvent); - twoEventsList.add(trackerEvent); - twoEventsList.add(trackerEvent); + singleEventList.add(trackerPayload); + twoEventsList.add(trackerPayload); + twoEventsList.add(trackerPayload); } @Test public void correctlyAddAnEventToStore() { - boolean result = eventStore.add(trackerEvent); + boolean result = eventStore.add(trackerPayload); Assert.assertTrue(result); } @Test public void getSize_returnsCorrectNumberOfStoredEvents() { - storeTwoEvents(); + storeTwoPayloads(); Assert.assertEquals(2, eventStore.getSize()); } @Test public void removeAddedEvent() { - storeTwoEvents(); + storeTwoPayloads(); - List removedEventList = eventStore.removeEvents(1); + List removedEventList = eventStore.removeEvents(1); Assert.assertEquals(singleEventList, removedEventList); Assert.assertEquals(1, eventStore.getSize()); } @Test public void removeAllEventsIfAskedForMoreEventsThanAreStored() { - storeTwoEvents(); + storeTwoPayloads(); - List removedEventList = eventStore.removeEvents(100); + List removedEventList = eventStore.removeEvents(100); Assert.assertEquals(twoEventsList, removedEventList); Assert.assertEquals(0, eventStore.getSize()); } @Test public void getAllEvents_doesNotRemoveEventsFromStore() { - storeTwoEvents(); + storeTwoPayloads(); - List retrievedEventsList = eventStore.getAllEvents(); + List retrievedEventsList = eventStore.getAllEvents(); Assert.assertEquals(twoEventsList, retrievedEventsList); Assert.assertEquals(2, eventStore.getSize()); } - private TrackerEvent createEvent() { + private TrackerPayload createPayload() { PageView pv = PageView.builder() .pageUrl("https://www.snowplowanalytics.com/") .pageTitle("Snowplow") .referrer("https://www.google.com/") .build(); - return new TrackerEvent(pv, new TrackerParameters("appId", DevicePlatform.ServerSideApp, "namespace", "0.0.0", false), null); + return pv.getPayload(); } - private void storeTwoEvents() { - for (TrackerEvent event : twoEventsList) { - eventStore.add(event); + private void storeTwoPayloads() { + for (TrackerPayload payload : twoEventsList) { + eventStore.add(payload); } } } From 864432654461ed9ef5a6360794543b88731e1235 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 27 Jan 2022 11:08:58 +0000 Subject: [PATCH 072/128] Provide method for stopping Tracker executorService (close #297) --- build.gradle | 2 +- .../snowplow/tracker/Tracker.java | 29 +++++++++++++++++++ .../tracker/emitter/BatchEmitter.java | 2 +- .../tracker/http/OkHttpClientAdapter.java | 4 +-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 6a1eb761..3d7b65c1 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.11.0' +version = '0.12.0-alpha.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 67e91d28..ae92a5eb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -16,16 +16,20 @@ import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Tracker { @@ -34,6 +38,7 @@ public class Tracker { private Subject subject; private final TrackerParameters parameters; protected ExecutorService executor; + private static final Logger LOGGER = LoggerFactory.getLogger(Tracker.class); /** * Creates a new Snowplow Tracker. @@ -375,4 +380,28 @@ private void addSubject(Event event, TrackerPayload payload) { payload.addMap(new HashMap<>(this.subject.getSubject())); } } + + public void close() { + // Shutdown executor thread pool for the tracker + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + executor.shutdownNow(); + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) + LOGGER.warn("Tracker executor did not terminate"); + } + } catch (InterruptedException ie) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // Shutdown executor thread pool for the emitter + if (this.emitter.getClass().equals(BatchEmitter.class)) { + BatchEmitter emitter = (BatchEmitter) this.emitter; + emitter.close(); + } + } + } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index a4b0d11d..4beb379c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -229,7 +229,7 @@ public void close() { if (!executor.awaitTermination(closeTimeout, TimeUnit.SECONDS)) { executor.shutdownNow(); if (!executor.awaitTermination(closeTimeout, TimeUnit.SECONDS)) - LOGGER.warn("Executor did not terminate"); + LOGGER.warn("Emitter executor did not terminate"); } } catch (final InterruptedException ie) { executor.shutdownNow(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 3e0a63e6..4c74e94d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -142,7 +142,7 @@ public int doPost(String url, String payload) { } catch (IOException e) { LOGGER.error("OkHttpClient POST Request failed: {}", e.getMessage()); } - + return returnValue; } -} \ No newline at end of file +} From ad8b063797692447c416145417573b0fc9d3ddf3 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 31 Jan 2022 10:53:12 +0000 Subject: [PATCH 073/128] Update simple-console example (close #295) * Remove bintray from demo build.gradle * Add simple-console demo to Build CI workflow * Add structured event to demo * Add an example Subject * Close tracker threads * Build demo differently in GH Build workflow * Remove sleep step --- .github/workflows/{gradle.yml => build.yml} | 6 ++ examples/simple-console/build.gradle | 9 +-- .../main/java/com/snowplowanalytics/Main.java | 73 ++++++++++++------- 3 files changed, 55 insertions(+), 33 deletions(-) rename .github/workflows/{gradle.yml => build.yml} (89%) diff --git a/.github/workflows/gradle.yml b/.github/workflows/build.yml similarity index 89% rename from .github/workflows/gradle.yml rename to .github/workflows/build.yml index 907c2f04..3a708a09 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,12 @@ jobs: - name: Test with Gradle Wrapper run: ./gradlew test + - name: Build simple-console example + run: | + ./gradlew publishToMavenLocal + cd examples/simple-console + ./gradlew build + - name: Upload report if failed if: ${{ failure() }} uses: actions/upload-artifact@v2 diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 0497ec92..97217503 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -6,9 +6,6 @@ targetCompatibility = '1.8' repositories { mavenLocal() - maven { - url "https://snowplow.bintray.com/snowplow-maven" - } mavenCentral() } @@ -19,11 +16,11 @@ test { } dependencies { - implementation 'com.snowplowanalytics:snowplow-java-tracker:0.10.1' + implementation 'com.snowplowanalytics:snowplow-java-tracker:0.+' - implementation ('com.snowplowanalytics:snowplow-java-tracker:0.10.1') { + implementation ('com.snowplowanalytics:snowplow-java-tracker:0.+') { capabilities { - requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support:0.10.1' + requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support' } } diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 037bb9d5..ba693590 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -14,17 +14,13 @@ package com.snowplowanalytics; import com.snowplowanalytics.snowplow.tracker.DevicePlatform; +import com.snowplowanalytics.snowplow.tracker.Subject; import com.snowplowanalytics.snowplow.tracker.Tracker; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; -import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import java.util.List; -import java.util.Set; -import java.util.HashSet; -import java.util.concurrent.TimeUnit; import static java.util.Collections.singletonList; import com.google.common.collect.ImmutableMap; @@ -39,11 +35,8 @@ public static String getUrlFromArgs(String[] args) { } public static void main(String[] args) { - Set failedEventIds = new HashSet(); String collectorEndpoint = getUrlFromArgs(args); - System.out.println("Sending events to " + collectorEndpoint); - // the application id to attach to events String appId = "java-tracker-sample-console-app"; // the namespace to attach to events @@ -52,7 +45,7 @@ public static void main(String[] args) { // build an emitter, this is used by the tracker to batch and schedule transmission of events BatchEmitter emitter = BatchEmitter.builder() .url(collectorEndpoint) - .bufferSize(4) // send an event every time one is given (no batching). In production this number should be higher, depending on the size/event volume + .bufferSize(4) // send batches of 4 events. In production this number should be higher, depending on the size/event volume .build(); // now we have the emitter, we need a tracker to turn our events into something a Snowplow collector can understand @@ -61,22 +54,32 @@ public static void main(String[] args) { .platform(DevicePlatform.ServerSideApp) .build(); - // This is an example of a custom context - List contexts = singletonList( + System.out.println("Sending events to " + collectorEndpoint); + System.out.println("Using tracker version " + tracker.getTrackerVersion()); + + // This is an example of a custom context entity + List context = singletonList( new SelfDescribingJson( "iglu:com.snowplowanalytics.iglu/anything-c/jsonschema/1-0-0", ImmutableMap.of("foo", "bar"))); - // This is a sample page view event, many other event types (such as self-describing events) are available + // This is an example of a eventSubject for adding user data + Subject eventSubject = new Subject.SubjectBuilder().build(); + eventSubject.setUserId("example@snowplowanalytics.com"); + eventSubject.setLanguage("EN"); + + // This is a sample page view event + // the eventSubject has been included in this event PageView pageViewEvent = PageView.builder() .pageTitle("Snowplow Analytics") .pageUrl("https://www.snowplowanalytics.com") .referrer("https://www.google.com") - .customContext(contexts) + .customContext(context) + .subject(eventSubject) .build(); - tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow - + // EcommerceTransactionItems are tracked as part of an EcommerceTransaction event + // They are processed into separate events during the `track()` call EcommerceTransactionItem item = EcommerceTransactionItem.builder() .itemId("order_id") .sku("sku") @@ -85,9 +88,10 @@ public static void main(String[] args) { .name("name") .category("category") .currency("currency") - .customContext(contexts) + .customContext(context) .build(); + // EcommerceTransaction event EcommerceTransaction ecommerceTransaction = EcommerceTransaction.builder() .orderId("order_id") .totalValue(1.0) @@ -98,31 +102,30 @@ public static void main(String[] args) { .state("state") .country("country") .currency("currency") - .items(item) // EcommerceTransactionItem events are added to a parent EcommerceTransaction - .customContext(contexts) + .items(item) // EcommerceTransactionItem events are added to a parent EcommerceTransaction here + .customContext(context) .build(); - tracker.track(ecommerceTransaction); // This will track two events - // This is an example of a custom "Unsutrcutred" event based on a schema + // This is an example of a custom "Unstructured" event based on a schema + // Unstructured events are also called "self-describing" events + // because of their SelfDescribingJson base Unstructured unstructured = Unstructured.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.iglu/anything-a/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") )) - .customContext(contexts) + .customContext(context) .build(); - tracker.track(unstructured); // This is an example of a ScreenView event which will be translated into an Unstructured event ScreenView screenView = ScreenView.builder() .name("name") .id("id") - .customContext(contexts) + .customContext(context) .build(); - tracker.track(screenView); // This is an example of a Timing event which will be translated into an Unstructured event Timing timing = Timing.builder() @@ -130,14 +133,30 @@ public static void main(String[] args) { .label("label") .variable("variable") .timing(10) - .customContext(contexts) + .customContext(context) .build(); + // This is an example of a Structured event + Structured structured = Structured.builder() + .category("category") + .action("action") + .label("label") + .property("property") + .value(12.34) + .customContext(context) + .build(); + + tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow + tracker.track(ecommerceTransaction); // This will track two events + tracker.track(unstructured); + tracker.track(screenView); tracker.track(timing); + tracker.track(structured); // Will close all threads and force send remaining events - // should be 1 left to flush, as we send 5 events with a bufferSize of 4 - emitter.close(); + tracker.close(); + + System.out.println("Tracked 7 events"); } } From f9cab8c3dcfeabb0c63875dad5489cc25b35a9dc Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 10 Feb 2022 15:16:59 +0000 Subject: [PATCH 074/128] Add benchmarking tests (close #300) * Set up JMH testing in main project * Add benchmark test * Create separate project for jmh * Remove JMH from main project * Fix memory leaks * Add comments and readme --- build.gradle | 1 + examples/benchmarking/BenchmarkingREADME.md | 24 +++ examples/benchmarking/build.gradle | 37 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/benchmarking/gradlew | 185 ++++++++++++++++++ examples/benchmarking/gradlew.bat | 89 +++++++++ examples/benchmarking/settings.gradle | 2 + .../snowplowanalytics/TrackerBenchmark.java | 125 ++++++++++++ 9 files changed, 468 insertions(+) create mode 100644 examples/benchmarking/BenchmarkingREADME.md create mode 100644 examples/benchmarking/build.gradle create mode 100644 examples/benchmarking/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/benchmarking/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/benchmarking/gradlew create mode 100644 examples/benchmarking/gradlew.bat create mode 100644 examples/benchmarking/settings.gradle create mode 100644 examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java diff --git a/build.gradle b/build.gradle index 3d7b65c1..15ba1289 100644 --- a/build.gradle +++ b/build.gradle @@ -218,3 +218,4 @@ signing { println 'Used useInMemoryPgpKeys()' } } + diff --git a/examples/benchmarking/BenchmarkingREADME.md b/examples/benchmarking/BenchmarkingREADME.md new file mode 100644 index 00000000..7e0ff9e8 --- /dev/null +++ b/examples/benchmarking/BenchmarkingREADME.md @@ -0,0 +1,24 @@ +## Benchmarking results + +This benchmarking module is provided for maintainers, allowing them to check that their changes have not degraded performance. It uses the Java microbenchmarking harness, JMH. + +The benchmark test measures the time taken to track one event. Note that this does not include the time for the event to be processed and sent, which happens asynchronously. + +To run the test, navigate to this folder and run: + +```bash +$ ./gradlew build +$ ./gradlew jmh +``` + +The tracker version is set in the `build.gradle` file. Change the specified version to benchmark a different tracker version. +```groovy +dependencies { + jmh 'com.snowplowanalytics:snowplow-java-tracker:0.11.0' +} +``` +Note that you may also need to edit the `TrackerBenchmark` `closeThreads()` code. Versions from 0.12.0 onwards must call a different method. This is explained in in-line comments. + +### Results +See this PR for discussion of benchmarking results: https://github.com/snowplow/snowplow-java-tracker/pull/301 + diff --git a/examples/benchmarking/build.gradle b/examples/benchmarking/build.gradle new file mode 100644 index 00000000..f5c3f773 --- /dev/null +++ b/examples/benchmarking/build.gradle @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +import org.gradle.api.tasks.JavaExec + +plugins { + id 'java' + id "me.champeau.jmh" version "0.6.6" +} + +group 'com.snowplowanalytics' +version '1.0' + +repositories { + mavenLocal { + content { + includeGroup "com.snowplowanalytics" + } + } + mavenCentral() +} + + +dependencies { + jmh 'com.snowplowanalytics:snowplow-java-tracker:0.10.1' +} + diff --git a/examples/benchmarking/gradle/wrapper/gradle-wrapper.jar b/examples/benchmarking/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 zcma&NbC71ylI~qywr$(CZQJHswz}-9F59+k+g;UV+cs{`J?GrGXYR~=-ydruB3JCa zB64N^cILAcWk5iofq)<(fq;O7{th4@;QxID0)qN`mJ?GIqLY#rX8-|G{5M0pdVW5^ zzXk$-2kQTAC?_N@B`&6-N-rmVFE=$QD?>*=4<|!MJu@}isLc4AW#{m2if&A5T5g&~ ziuMQeS*U5sL6J698wOd)K@oK@1{peP5&Esut<#VH^u)gp`9H4)`uE!2$>RTctN+^u z=ASkePDZA-X8)rp%D;p*~P?*a_=*Kwc<^>QSH|^<0>o37lt^+Mj1;4YvJ(JR-Y+?%Nu}JAYj5 z_Qc5%Ao#F?q32i?ZaN2OSNhWL;2oDEw_({7ZbgUjna!Fqn3NzLM@-EWFPZVmc>(fZ z0&bF-Ch#p9C{YJT9Rcr3+Y_uR^At1^BxZ#eo>$PLJF3=;t_$2|t+_6gg5(j{TmjYU zK12c&lE?Eh+2u2&6Gf*IdKS&6?rYbSEKBN!rv{YCm|Rt=UlPcW9j`0o6{66#y5t9C zruFA2iKd=H%jHf%ypOkxLnO8#H}#Zt{8p!oi6)7#NqoF({t6|J^?1e*oxqng9Q2Cc zg%5Vu!em)}Yuj?kaP!D?b?(C*w!1;>R=j90+RTkyEXz+9CufZ$C^umX^+4|JYaO<5 zmIM3#dv`DGM;@F6;(t!WngZSYzHx?9&$xEF70D1BvfVj<%+b#)vz)2iLCrTeYzUcL z(OBnNoG6Le%M+@2oo)&jdOg=iCszzv59e zDRCeaX8l1hC=8LbBt|k5?CXgep=3r9BXx1uR8!p%Z|0+4Xro=xi0G!e{c4U~1j6!) zH6adq0}#l{%*1U(Cb%4AJ}VLWKBPi0MoKFaQH6x?^hQ!6em@993xdtS%_dmevzeNl z(o?YlOI=jl(`L9^ z0O+H9k$_@`6L13eTT8ci-V0ljDMD|0ifUw|Q-Hep$xYj0hTO@0%IS^TD4b4n6EKDG z??uM;MEx`s98KYN(K0>c!C3HZdZ{+_53DO%9k5W%pr6yJusQAv_;IA}925Y%;+!tY z%2k!YQmLLOr{rF~!s<3-WEUs)`ix_mSU|cNRBIWxOox_Yb7Z=~Q45ZNe*u|m^|)d* zog=i>`=bTe!|;8F+#H>EjIMcgWcG2ORD`w0WD;YZAy5#s{65~qfI6o$+Ty&-hyMyJ z3Ra~t>R!p=5ZpxA;QkDAoPi4sYOP6>LT+}{xp}tk+<0k^CKCFdNYG(Es>p0gqD)jP zWOeX5G;9(m@?GOG7g;e74i_|SmE?`B2i;sLYwRWKLy0RLW!Hx`=!LH3&k=FuCsM=9M4|GqzA)anEHfxkB z?2iK-u(DC_T1};KaUT@3nP~LEcENT^UgPvp!QC@Dw&PVAhaEYrPey{nkcn(ro|r7XUz z%#(=$7D8uP_uU-oPHhd>>^adbCSQetgSG`e$U|7mr!`|bU0aHl_cmL)na-5x1#OsVE#m*+k84Y^+UMeSAa zbrVZHU=mFwXEaGHtXQq`2ZtjfS!B2H{5A<3(nb-6ARVV8kEmOkx6D2x7~-6hl;*-*}2Xz;J#a8Wn;_B5=m zl3dY;%krf?i-Ok^Pal-}4F`{F@TYPTwTEhxpZK5WCpfD^UmM_iYPe}wpE!Djai6_{ z*pGO=WB47#Xjb7!n2Ma)s^yeR*1rTxp`Mt4sfA+`HwZf%!7ZqGosPkw69`Ix5Ku6G z@Pa;pjzV&dn{M=QDx89t?p?d9gna*}jBly*#1!6}5K<*xDPJ{wv4& zM$17DFd~L*Te3A%yD;Dp9UGWTjRxAvMu!j^Tbc}2v~q^59d4bz zvu#!IJCy(BcWTc`;v$9tH;J%oiSJ_i7s;2`JXZF+qd4C)vY!hyCtl)sJIC{ebI*0> z@x>;EzyBv>AI-~{D6l6{ST=em*U( z(r$nuXY-#CCi^8Z2#v#UXOt`dbYN1z5jzNF2 z411?w)whZrfA20;nl&C1Gi+gk<`JSm+{|*2o<< zqM#@z_D`Cn|0H^9$|Tah)0M_X4c37|KQ*PmoT@%xHc3L1ZY6(p(sNXHa&49Frzto& zR`c~ClHpE~4Z=uKa5S(-?M8EJ$zt0&fJk~p$M#fGN1-y$7!37hld`Uw>Urri(DxLa;=#rK0g4J)pXMC zxzraOVw1+kNWpi#P=6(qxf`zSdUC?D$i`8ZI@F>k6k zz21?d+dw7b&i*>Kv5L(LH-?J%@WnqT7j#qZ9B>|Zl+=> z^U-pV@1y_ptHo4hl^cPRWewbLQ#g6XYQ@EkiP z;(=SU!yhjHp%1&MsU`FV1Z_#K1&(|5n(7IHbx&gG28HNT)*~-BQi372@|->2Aw5It z0CBpUcMA*QvsPy)#lr!lIdCi@1k4V2m!NH)%Px(vu-r(Q)HYc!p zJ^$|)j^E#q#QOgcb^pd74^JUi7fUmMiNP_o*lvx*q%_odv49Dsv$NV;6J z9GOXKomA{2Pb{w}&+yHtH?IkJJu~}Z?{Uk++2mB8zyvh*xhHKE``99>y#TdD z&(MH^^JHf;g(Tbb^&8P*;_i*2&fS$7${3WJtV7K&&(MBV2~)2KB3%cWg#1!VE~k#C z!;A;?p$s{ihyojEZz+$I1)L}&G~ml=udD9qh>Tu(ylv)?YcJT3ihapi!zgPtWb*CP zlLLJSRCj-^w?@;RU9aL2zDZY1`I3d<&OMuW=c3$o0#STpv_p3b9Wtbql>w^bBi~u4 z3D8KyF?YE?=HcKk!xcp@Cigvzy=lnFgc^9c%(^F22BWYNAYRSho@~*~S)4%AhEttv zvq>7X!!EWKG?mOd9&n>vvH1p4VzE?HCuxT-u+F&mnsfDI^}*-d00-KAauEaXqg3k@ zy#)MGX!X;&3&0s}F3q40ZmVM$(H3CLfpdL?hB6nVqMxX)q=1b}o_PG%r~hZ4gUfSp zOH4qlEOW4OMUc)_m)fMR_rl^pCfXc{$fQbI*E&mV77}kRF z&{<06AJyJ!e863o-V>FA1a9Eemx6>^F$~9ppt()ZbPGfg_NdRXBWoZnDy2;#ODgf! zgl?iOcF7Meo|{AF>KDwTgYrJLb$L2%%BEtO>T$C?|9bAB&}s;gI?lY#^tttY&hfr# zKhC+&b-rpg_?~uVK%S@mQleU#_xCsvIPK*<`E0fHE1&!J7!xD#IB|SSPW6-PyuqGn3^M^Rz%WT{e?OI^svARX&SAdU77V(C~ zM$H{Kg59op{<|8ry9ecfP%=kFm(-!W&?U0@<%z*+!*<e0XesMxRFu9QnGqun6R_%T+B%&9Dtk?*d$Q zb~>84jEAPi@&F@3wAa^Lzc(AJz5gsfZ7J53;@D<;Klpl?sK&u@gie`~vTsbOE~Cd4 z%kr56mI|#b(Jk&;p6plVwmNB0H@0SmgdmjIn5Ne@)}7Vty(yb2t3ev@22AE^s!KaN zyQ>j+F3w=wnx7w@FVCRe+`vUH)3gW%_72fxzqX!S&!dchdkRiHbXW1FMrIIBwjsai8`CB2r4mAbwp%rrO>3B$Zw;9=%fXI9B{d(UzVap7u z6piC-FQ)>}VOEuPpuqznpY`hN4dGa_1Xz9rVg(;H$5Te^F0dDv*gz9JS<|>>U0J^# z6)(4ICh+N_Q`Ft0hF|3fSHs*?a=XC;e`sJaU9&d>X4l?1W=|fr!5ShD|nv$GK;j46@BV6+{oRbWfqOBRb!ir88XD*SbC(LF}I1h#6@dvK%Toe%@ zhDyG$93H8Eu&gCYddP58iF3oQH*zLbNI;rN@E{T9%A8!=v#JLxKyUe}e}BJpB{~uN zqgxRgo0*-@-iaHPV8bTOH(rS(huwK1Xg0u+e!`(Irzu@Bld&s5&bWgVc@m7;JgELd zimVs`>vQ}B_1(2#rv#N9O`fJpVfPc7V2nv34PC);Dzbb;p!6pqHzvy?2pD&1NE)?A zt(t-ucqy@wn9`^MN5apa7K|L=9>ISC>xoc#>{@e}m#YAAa1*8-RUMKwbm|;5p>T`Z zNf*ph@tnF{gmDa3uwwN(g=`Rh)4!&)^oOy@VJaK4lMT&5#YbXkl`q?<*XtsqD z9PRK6bqb)fJw0g-^a@nu`^?71k|m3RPRjt;pIkCo1{*pdqbVs-Yl>4E>3fZx3Sv44grW=*qdSoiZ9?X0wWyO4`yDHh2E!9I!ZFi zVL8|VtW38}BOJHW(Ax#KL_KQzarbuE{(%TA)AY)@tY4%A%P%SqIU~8~-Lp3qY;U-} z`h_Gel7;K1h}7$_5ZZT0&%$Lxxr-<89V&&TCsu}LL#!xpQ1O31jaa{U34~^le*Y%L za?7$>Jk^k^pS^_M&cDs}NgXlR>16AHkSK-4TRaJSh#h&p!-!vQY%f+bmn6x`4fwTp z$727L^y`~!exvmE^W&#@uY!NxJi`g!i#(++!)?iJ(1)2Wk;RN zFK&O4eTkP$Xn~4bB|q8y(btx$R#D`O@epi4ofcETrx!IM(kWNEe42Qh(8*KqfP(c0 zouBl6>Fc_zM+V;F3znbo{x#%!?mH3`_ANJ?y7ppxS@glg#S9^MXu|FM&ynpz3o&Qh z2ujAHLF3($pH}0jXQsa#?t--TnF1P73b?4`KeJ9^qK-USHE)4!IYgMn-7z|=ALF5SNGkrtPG@Y~niUQV2?g$vzJN3nZ{7;HZHzWAeQ;5P|@Tl3YHpyznGG4-f4=XflwSJY+58-+wf?~Fg@1p1wkzuu-RF3j2JX37SQUc? zQ4v%`V8z9ZVZVqS8h|@@RpD?n0W<=hk=3Cf8R?d^9YK&e9ZybFY%jdnA)PeHvtBe- zhMLD+SSteHBq*q)d6x{)s1UrsO!byyLS$58WK;sqip$Mk{l)Y(_6hEIBsIjCr5t>( z7CdKUrJTrW%qZ#1z^n*Lb8#VdfzPw~OIL76aC+Rhr<~;4Tl!sw?Rj6hXj4XWa#6Tp z@)kJ~qOV)^Rh*-?aG>ic2*NlC2M7&LUzc9RT6WM%Cpe78`iAowe!>(T0jo&ivn8-7 zs{Qa@cGy$rE-3AY0V(l8wjI^uB8Lchj@?L}fYal^>T9z;8juH@?rG&g-t+R2dVDBe zq!K%{e-rT5jX19`(bP23LUN4+_zh2KD~EAYzhpEO3MUG8@}uBHH@4J zd`>_(K4q&>*k82(dDuC)X6JuPrBBubOg7qZ{?x!r@{%0);*`h*^F|%o?&1wX?Wr4b z1~&cy#PUuES{C#xJ84!z<1tp9sfrR(i%Tu^jnXy;4`Xk;AQCdFC@?V%|; zySdC7qS|uQRcH}EFZH%mMB~7gi}a0utE}ZE_}8PQH8f;H%PN41Cb9R%w5Oi5el^fd z$n{3SqLCnrF##x?4sa^r!O$7NX!}&}V;0ZGQ&K&i%6$3C_dR%I7%gdQ;KT6YZiQrW zk%q<74oVBV>@}CvJ4Wj!d^?#Zwq(b$E1ze4$99DuNg?6t9H}k_|D7KWD7i0-g*EO7 z;5{hSIYE4DMOK3H%|f5Edx+S0VI0Yw!tsaRS2&Il2)ea^8R5TG72BrJue|f_{2UHa z@w;^c|K3da#$TB0P3;MPlF7RuQeXT$ zS<<|C0OF(k)>fr&wOB=gP8!Qm>F41u;3esv7_0l%QHt(~+n; zf!G6%hp;Gfa9L9=AceiZs~tK+Tf*Wof=4!u{nIO90jH@iS0l+#%8=~%ASzFv7zqSB^?!@N7)kp0t&tCGLmzXSRMRyxCmCYUD2!B`? zhs$4%KO~m=VFk3Buv9osha{v+mAEq=ik3RdK@;WWTV_g&-$U4IM{1IhGX{pAu%Z&H zFfwCpUsX%RKg);B@7OUzZ{Hn{q6Vv!3#8fAg!P$IEx<0vAx;GU%}0{VIsmFBPq_mb zpe^BChDK>sc-WLKl<6 zwbW|e&d&dv9Wu0goueyu>(JyPx1mz0v4E?cJjFuKF71Q1)AL8jHO$!fYT3(;U3Re* zPPOe%*O+@JYt1bW`!W_1!mN&=w3G9ru1XsmwfS~BJ))PhD(+_J_^N6j)sx5VwbWK| zwRyC?W<`pOCY)b#AS?rluxuuGf-AJ=D!M36l{ua?@SJ5>e!IBr3CXIxWw5xUZ@Xrw z_R@%?{>d%Ld4p}nEsiA@v*nc6Ah!MUs?GA7e5Q5lPpp0@`%5xY$C;{%rz24$;vR#* zBP=a{)K#CwIY%p} zXVdxTQ^HS@O&~eIftU+Qt^~(DGxrdi3k}DdT^I7Iy5SMOp$QuD8s;+93YQ!OY{eB24%xY7ml@|M7I(Nb@K_-?F;2?et|CKkuZK_>+>Lvg!>JE~wN`BI|_h6$qi!P)+K-1Hh(1;a`os z55)4Q{oJiA(lQM#;w#Ta%T0jDNXIPM_bgESMCDEg6rM33anEr}=|Fn6)|jBP6Y}u{ zv9@%7*#RI9;fv;Yii5CI+KrRdr0DKh=L>)eO4q$1zmcSmglsV`*N(x=&Wx`*v!!hn6X-l0 zP_m;X??O(skcj+oS$cIdKhfT%ABAzz3w^la-Ucw?yBPEC+=Pe_vU8nd-HV5YX6X8r zZih&j^eLU=%*;VzhUyoLF;#8QsEfmByk+Y~caBqSvQaaWf2a{JKB9B>V&r?l^rXaC z8)6AdR@Qy_BxQrE2Fk?ewD!SwLuMj@&d_n5RZFf7=>O>hzVE*seW3U?_p|R^CfoY`?|#x9)-*yjv#lo&zP=uI`M?J zbzC<^3x7GfXA4{FZ72{PE*-mNHyy59Q;kYG@BB~NhTd6pm2Oj=_ zizmD?MKVRkT^KmXuhsk?eRQllPo2Ubk=uCKiZ&u3Xjj~<(!M94c)Tez@9M1Gfs5JV z->@II)CDJOXTtPrQudNjE}Eltbjq>6KiwAwqvAKd^|g!exgLG3;wP+#mZYr`cy3#39e653d=jrR-ulW|h#ddHu(m9mFoW~2yE zz5?dB%6vF}+`-&-W8vy^OCxm3_{02royjvmwjlp+eQDzFVEUiyO#gLv%QdDSI#3W* z?3!lL8clTaNo-DVJw@ynq?q!%6hTQi35&^>P85G$TqNt78%9_sSJt2RThO|JzM$iL zg|wjxdMC2|Icc5rX*qPL(coL!u>-xxz-rFiC!6hD1IR%|HSRsV3>Kq~&vJ=s3M5y8SG%YBQ|{^l#LGlg!D?E>2yR*eV%9m$_J6VGQ~AIh&P$_aFbh zULr0Z$QE!QpkP=aAeR4ny<#3Fwyw@rZf4?Ewq`;mCVv}xaz+3ni+}a=k~P+yaWt^L z@w67!DqVf7D%7XtXX5xBW;Co|HvQ8WR1k?r2cZD%U;2$bsM%u8{JUJ5Z0k= zZJARv^vFkmWx15CB=rb=D4${+#DVqy5$C%bf`!T0+epLJLnh1jwCdb*zuCL}eEFvE z{rO1%gxg>1!W(I!owu*mJZ0@6FM(?C+d*CeceZRW_4id*D9p5nzMY&{mWqrJomjIZ z97ZNnZ3_%Hx8dn;H>p8m7F#^2;T%yZ3H;a&N7tm=Lvs&lgJLW{V1@h&6Vy~!+Ffbb zv(n3+v)_D$}dqd!2>Y2B)#<+o}LH#%ogGi2-?xRIH)1!SD)u-L65B&bsJTC=LiaF+YOCif2dUX6uAA|#+vNR z>U+KQekVGon)Yi<93(d!(yw1h3&X0N(PxN2{%vn}cnV?rYw z$N^}_o!XUB!mckL`yO1rnUaI4wrOeQ(+&k?2mi47hzxSD`N#-byqd1IhEoh!PGq>t z_MRy{5B0eKY>;Ao3z$RUU7U+i?iX^&r739F)itdrTpAi-NN0=?^m%?{A9Ly2pVv>Lqs6moTP?T2-AHqFD-o_ znVr|7OAS#AEH}h8SRPQ@NGG47dO}l=t07__+iK8nHw^(AHx&Wb<%jPc$$jl6_p(b$ z)!pi(0fQodCHfM)KMEMUR&UID>}m^(!{C^U7sBDOA)$VThRCI0_+2=( zV8mMq0R(#z;C|7$m>$>`tX+T|xGt(+Y48@ZYu#z;0pCgYgmMVbFb!$?%yhZqP_nhn zy4<#3P1oQ#2b51NU1mGnHP$cf0j-YOgAA}A$QoL6JVLcmExs(kU{4z;PBHJD%_=0F z>+sQV`mzijSIT7xn%PiDKHOujX;n|M&qr1T@rOxTdxtZ!&u&3HHFLYD5$RLQ=heur zb>+AFokUVQeJy-#LP*^)spt{mb@Mqe=A~-4p0b+Bt|pZ+@CY+%x}9f}izU5;4&QFE zO1bhg&A4uC1)Zb67kuowWY4xbo&J=%yoXlFB)&$d*-}kjBu|w!^zbD1YPc0-#XTJr z)pm2RDy%J3jlqSMq|o%xGS$bPwn4AqitC6&e?pqWcjWPt{3I{>CBy;hg0Umh#c;hU3RhCUX=8aR>rmd` z7Orw(5tcM{|-^J?ZAA9KP|)X6n9$-kvr#j5YDecTM6n z&07(nD^qb8hpF0B^z^pQ*%5ePYkv&FabrlI61ntiVp!!C8y^}|<2xgAd#FY=8b*y( zuQOuvy2`Ii^`VBNJB&R!0{hABYX55ooCAJSSevl4RPqEGb)iy_0H}v@vFwFzD%>#I>)3PsouQ+_Kkbqy*kKdHdfkN7NBcq%V{x^fSxgXpg7$bF& zj!6AQbDY(1u#1_A#1UO9AxiZaCVN2F0wGXdY*g@x$ByvUA?ePdide0dmr#}udE%K| z3*k}Vv2Ew2u1FXBaVA6aerI36R&rzEZeDDCl5!t0J=ug6kuNZzH>3i_VN`%BsaVB3 zQYw|Xub_SGf{)F{$ZX5`Jc!X!;eybjP+o$I{Z^Hsj@D=E{MnnL+TbC@HEU2DjG{3-LDGIbq()U87x4eS;JXnSh;lRlJ z>EL3D>wHt-+wTjQF$fGyDO$>d+(fq@bPpLBS~xA~R=3JPbS{tzN(u~m#Po!?H;IYv zE;?8%^vle|%#oux(Lj!YzBKv+Fd}*Ur-dCBoX*t{KeNM*n~ZPYJ4NNKkI^MFbz9!v z4(Bvm*Kc!-$%VFEewYJKz-CQN{`2}KX4*CeJEs+Q(!kI%hN1!1P6iOq?ovz}X0IOi z)YfWpwW@pK08^69#wSyCZkX9?uZD?C^@rw^Y?gLS_xmFKkooyx$*^5#cPqntNTtSG zlP>XLMj2!VF^0k#ole7`-c~*~+_T5ls?x4)ah(j8vo_ zwb%S8qoaZqY0-$ZI+ViIA_1~~rAH7K_+yFS{0rT@eQtTAdz#8E5VpwnW!zJ_^{Utv zlW5Iar3V5t&H4D6A=>?mq;G92;1cg9a2sf;gY9pJDVKn$DYdQlvfXq}zz8#LyPGq@ z+`YUMD;^-6w&r-82JL7mA8&M~Pj@aK!m{0+^v<|t%APYf7`}jGEhdYLqsHW-Le9TL z_hZZ1gbrz7$f9^fAzVIP30^KIz!!#+DRLL+qMszvI_BpOSmjtl$hh;&UeM{ER@INV zcI}VbiVTPoN|iSna@=7XkP&-4#06C};8ajbxJ4Gcq8(vWv4*&X8bM^T$mBk75Q92j z1v&%a;OSKc8EIrodmIiw$lOES2hzGDcjjB`kEDfJe{r}yE6`eZL zEB`9u>Cl0IsQ+t}`-cx}{6jqcANucqIB>Qmga_&<+80E2Q|VHHQ$YlAt{6`Qu`HA3 z03s0-sSlwbvgi&_R8s={6<~M^pGvBNjKOa>tWenzS8s zR>L7R5aZ=mSU{f?ib4Grx$AeFvtO5N|D>9#)ChH#Fny2maHWHOf2G=#<9Myot#+4u zWVa6d^Vseq_0=#AYS(-m$Lp;*8nC_6jXIjEM`omUmtH@QDs3|G)i4j*#_?#UYVZvJ z?YjT-?!4Q{BNun;dKBWLEw2C-VeAz`%?A>p;)PL}TAZn5j~HK>v1W&anteARlE+~+ zj>c(F;?qO3pXBb|#OZdQnm<4xWmn~;DR5SDMxt0UK_F^&eD|KZ=O;tO3vy4@4h^;2 zUL~-z`-P1aOe?|ZC1BgVsL)2^J-&vIFI%q@40w0{jjEfeVl)i9(~bt2z#2Vm)p`V_ z1;6$Ae7=YXk#=Qkd24Y23t&GvRxaOoad~NbJ+6pxqzJ>FY#Td7@`N5xp!n(c!=RE& z&<<@^a$_Ys8jqz4|5Nk#FY$~|FPC0`*a5HH!|Gssa9=~66&xG9)|=pOOJ2KE5|YrR zw!w6K2aC=J$t?L-;}5hn6mHd%hC;p8P|Dgh6D>hGnXPgi;6r+eA=?f72y9(Cf_ho{ zH6#)uD&R=73^$$NE;5piWX2bzR67fQ)`b=85o0eOLGI4c-Tb@-KNi2pz=Ke@SDcPn za$AxXib84`!Sf;Z3B@TSo`Dz7GM5Kf(@PR>Ghzi=BBxK8wRp>YQoXm+iL>H*Jo9M3 z6w&E?BC8AFTFT&Tv8zf+m9<&S&%dIaZ)Aoqkak_$r-2{$d~0g2oLETx9Y`eOAf14QXEQw3tJne;fdzl@wV#TFXSLXM2428F-Q}t+n2g%vPRMUzYPvzQ9f# zu(liiJem9P*?0%V@RwA7F53r~|I!Ty)<*AsMX3J{_4&}{6pT%Tpw>)^|DJ)>gpS~1rNEh z0$D?uO8mG?H;2BwM5a*26^7YO$XjUm40XmBsb63MoR;bJh63J;OngS5sSI+o2HA;W zdZV#8pDpC9Oez&L8loZO)MClRz!_!WD&QRtQxnazhT%Vj6Wl4G11nUk8*vSeVab@N#oJ}`KyJv+8Mo@T1-pqZ1t|?cnaVOd;1(h9 z!$DrN=jcGsVYE-0-n?oCJ^4x)F}E;UaD-LZUIzcD?W^ficqJWM%QLy6QikrM1aKZC zi{?;oKwq^Vsr|&`i{jIphA8S6G4)$KGvpULjH%9u(Dq247;R#l&I0{IhcC|oBF*Al zvLo7Xte=C{aIt*otJD}BUq)|_pdR>{zBMT< z(^1RpZv*l*m*OV^8>9&asGBo8h*_4q*)-eCv*|Pq=XNGrZE)^(SF7^{QE_~4VDB(o zVcPA_!G+2CAtLbl+`=Q~9iW`4ZRLku!uB?;tWqVjB0lEOf}2RD7dJ=BExy=<9wkb- z9&7{XFA%n#JsHYN8t5d~=T~5DcW4$B%3M+nNvC2`0!#@sckqlzo5;hhGi(D9=*A4` z5ynobawSPRtWn&CDLEs3Xf`(8^zDP=NdF~F^s&={l7(aw&EG}KWpMjtmz7j_VLO;@ zM2NVLDxZ@GIv7*gzl1 zjq78tv*8#WSY`}Su0&C;2F$Ze(q>F(@Wm^Gw!)(j;dk9Ad{STaxn)IV9FZhm*n+U} zi;4y*3v%A`_c7a__DJ8D1b@dl0Std3F||4Wtvi)fCcBRh!X9$1x!_VzUh>*S5s!oq z;qd{J_r79EL2wIeiGAqFstWtkfIJpjVh%zFo*=55B9Zq~y0=^iqHWfQl@O!Ak;(o*m!pZqe9 z%U2oDOhR)BvW8&F70L;2TpkzIutIvNQaTjjs5V#8mV4!NQ}zN=i`i@WI1z0eN-iCS z;vL-Wxc^Vc_qK<5RPh(}*8dLT{~GzE{w2o$2kMFaEl&q zP{V=>&3kW7tWaK-Exy{~`v4J0U#OZBk{a9{&)&QG18L@6=bsZ1zC_d{{pKZ-Ey>I> z;8H0t4bwyQqgu4hmO`3|4K{R*5>qnQ&gOfdy?z`XD%e5+pTDzUt3`k^u~SaL&XMe= z9*h#kT(*Q9jO#w2Hd|Mr-%DV8i_1{J1MU~XJ3!WUplhXDYBpJH><0OU`**nIvPIof z|N8@I=wA)sf45SAvx||f?Z5uB$kz1qL3Ky_{%RPdP5iN-D2!p5scq}buuC00C@jom zhfGKm3|f?Z0iQ|K$Z~!`8{nmAS1r+fp6r#YDOS8V*;K&Gs7Lc&f^$RC66O|)28oh`NHy&vq zJh+hAw8+ybTB0@VhWN^0iiTnLsCWbS_y`^gs!LX!Lw{yE``!UVzrV24tP8o;I6-65 z1MUiHw^{bB15tmrVT*7-#sj6cs~z`wk52YQJ*TG{SE;KTm#Hf#a~|<(|ImHH17nNM z`Ub{+J3dMD!)mzC8b(2tZtokKW5pAwHa?NFiso~# z1*iaNh4lQ4TS)|@G)H4dZV@l*Vd;Rw;-;odDhW2&lJ%m@jz+Panv7LQm~2Js6rOW3 z0_&2cW^b^MYW3)@o;neZ<{B4c#m48dAl$GCc=$>ErDe|?y@z`$uq3xd(%aAsX)D%l z>y*SQ%My`yDP*zof|3@_w#cjaW_YW4BdA;#Glg1RQcJGY*CJ9`H{@|D+*e~*457kd z73p<%fB^PV!Ybw@)Dr%(ZJbX}xmCStCYv#K3O32ej{$9IzM^I{6FJ8!(=azt7RWf4 z7ib0UOPqN40X!wOnFOoddd8`!_IN~9O)#HRTyjfc#&MCZ zZAMzOVB=;qwt8gV?{Y2?b=iSZG~RF~uyx18K)IDFLl})G1v@$(s{O4@RJ%OTJyF+Cpcx4jmy|F3euCnMK!P2WTDu5j z{{gD$=M*pH!GGzL%P)V2*ROm>!$Y=z|D`!_yY6e7SU$~a5q8?hZGgaYqaiLnkK%?0 zs#oI%;zOxF@g*@(V4p!$7dS1rOr6GVs6uYCTt2h)eB4?(&w8{#o)s#%gN@BBosRUe z)@P@8_Zm89pr~)b>e{tbPC~&_MR--iB{=)y;INU5#)@Gix-YpgP<-c2Ms{9zuCX|3 z!p(?VaXww&(w&uBHzoT%!A2=3HAP>SDxcljrego7rY|%hxy3XlODWffO_%g|l+7Y_ zqV(xbu)s4lV=l7M;f>vJl{`6qBm>#ZeMA}kXb97Z)?R97EkoI?x6Lp0yu1Z>PS?2{ z0QQ(8D)|lc9CO3B~e(pQM&5(1y&y=e>C^X$`)_&XuaI!IgDTVqt31wX#n+@!a_A0ZQkA zCJ2@M_4Gb5MfCrm5UPggeyh)8 zO9?`B0J#rkoCx(R0I!ko_2?iO@|oRf1;3r+i)w-2&j?=;NVIdPFsB)`|IC0zk6r9c zRrkfxWsiJ(#8QndNJj@{@WP2Ackr|r1VxV{7S&rSU(^)-M8gV>@UzOLXu9K<{6e{T zXJ6b92r$!|lwjhmgqkdswY&}c)KW4A)-ac%sU;2^fvq7gfUW4Bw$b!i@duy1CAxSn z(pyh$^Z=&O-q<{bZUP+$U}=*#M9uVc>CQVgDs4swy5&8RAHZ~$)hrTF4W zPsSa~qYv_0mJnF89RnnJTH`3}w4?~epFl=D(35$ zWa07ON$`OMBOHgCmfO(9RFc<)?$x)N}Jd2A(<*Ll7+4jrRt9w zwGxExUXd9VB#I|DwfxvJ;HZ8Q{37^wDhaZ%O!oO(HpcqfLH%#a#!~;Jl7F5>EX_=8 z{()l2NqPz>La3qJR;_v+wlK>GsHl;uRA8%j`A|yH@k5r%55S9{*Cp%uw6t`qc1!*T za2OeqtQj7sAp#Q~=5Fs&aCR9v>5V+s&RdNvo&H~6FJOjvaj--2sYYBvMq;55%z8^o z|BJDA4vzfow#DO#ZQHh;Oq_{r+qP{R9ox2TOgwQiv7Ow!zjN+A@BN;0tA2lUb#+zO z(^b89eV)D7UVE+h{mcNc6&GtpOqDn_?VAQ)Vob$hlFwW%xh>D#wml{t&Ofmm_d_+; zKDxzdr}`n2Rw`DtyIjrG)eD0vut$}dJAZ0AohZ+ZQdWXn_Z@dI_y=7t3q8x#pDI-K z2VVc&EGq445Rq-j0=U=Zx`oBaBjsefY;%)Co>J3v4l8V(T8H?49_@;K6q#r~Wwppc z4XW0(4k}cP=5ex>-Xt3oATZ~bBWKv)aw|I|Lx=9C1s~&b77idz({&q3T(Y(KbWO?+ zmcZ6?WeUsGk6>km*~234YC+2e6Zxdl~<_g2J|IE`GH%n<%PRv-50; zH{tnVts*S5*_RxFT9eM0z-pksIb^drUq4>QSww=u;UFCv2AhOuXE*V4z?MM`|ABOC4P;OfhS(M{1|c%QZ=!%rQTDFx`+}?Kdx$&FU?Y<$x;j7z=(;Lyz+?EE>ov!8vvMtSzG!nMie zsBa9t8as#2nH}n8xzN%W%U$#MHNXmDUVr@GX{?(=yI=4vks|V)!-W5jHsU|h_&+kY zS_8^kd3jlYqOoiI`ZqBVY!(UfnAGny!FowZWY_@YR0z!nG7m{{)4OS$q&YDyw6vC$ zm4!$h>*|!2LbMbxS+VM6&DIrL*X4DeMO!@#EzMVfr)e4Tagn~AQHIU8?e61TuhcKD zr!F4(kEebk(Wdk-?4oXM(rJwanS>Jc%<>R(siF+>+5*CqJLecP_we33iTFTXr6W^G z7M?LPC-qFHK;E!fxCP)`8rkxZyFk{EV;G-|kwf4b$c1k0atD?85+|4V%YATWMG|?K zLyLrws36p%Qz6{}>7b>)$pe>mR+=IWuGrX{3ZPZXF3plvuv5Huax86}KX*lbPVr}L z{C#lDjdDeHr~?l|)Vp_}T|%$qF&q#U;ClHEPVuS+Jg~NjC1RP=17=aQKGOcJ6B3mp z8?4*-fAD~}sX*=E6!}^u8)+m2j<&FSW%pYr_d|p_{28DZ#Cz0@NF=gC-o$MY?8Ca8 zr5Y8DSR^*urS~rhpX^05r30Ik#2>*dIOGxRm0#0YX@YQ%Mg5b6dXlS!4{7O_kdaW8PFSdj1=ryI-=5$fiieGK{LZ+SX(1b=MNL!q#lN zv98?fqqTUH8r8C7v(cx#BQ5P9W>- zmW93;eH6T`vuJ~rqtIBg%A6>q>gnWb3X!r0wh_q;211+Om&?nvYzL1hhtjB zK_7G3!n7PL>d!kj){HQE zE8(%J%dWLh1_k%gVXTZt zEdT09XSKAx27Ncaq|(vzL3gm83q>6CAw<$fTnMU05*xAe&rDfCiu`u^1)CD<>sx0i z*hr^N_TeN89G(nunZoLBf^81#pmM}>JgD@Nn1l*lN#a=B=9pN%tmvYFjFIoKe_(GF z-26x{(KXdfsQL7Uv6UtDuYwV`;8V3w>oT_I<`Ccz3QqK9tYT5ZQzbop{=I=!pMOCb zCU68`n?^DT%^&m>A%+-~#lvF!7`L7a{z<3JqIlk1$<||_J}vW1U9Y&eX<}l8##6i( zZcTT@2`9(Mecptm@{3A_Y(X`w9K0EwtPq~O!16bq{7c0f7#(3wn-^)h zxV&M~iiF!{-6A@>o;$RzQ5A50kxXYj!tcgme=Qjrbje~;5X2xryU;vH|6bE(8z^<7 zQ>BG7_c*JG8~K7Oe68i#0~C$v?-t@~@r3t2inUnLT(c=URpA9kA8uq9PKU(Ps(LVH zqgcqW>Gm?6oV#AldDPKVRcEyQIdTT`Qa1j~vS{<;SwyTdr&3*t?J)y=M7q*CzucZ&B0M=joT zBbj@*SY;o2^_h*>R0e({!QHF0=)0hOj^B^d*m>SnRrwq>MolNSgl^~r8GR#mDWGYEIJA8B<|{{j?-7p zVnV$zancW3&JVDtVpIlI|5djKq0(w$KxEFzEiiL=h5Jw~4Le23@s(mYyXWL9SX6Ot zmb)sZaly_P%BeX_9 zw&{yBef8tFm+%=--m*J|o~+Xg3N+$IH)t)=fqD+|fEk4AAZ&!wcN5=mi~Vvo^i`}> z#_3ahR}Ju)(Px7kev#JGcSwPXJ2id9%Qd2A#Uc@t8~egZ8;iC{e! z%=CGJOD1}j!HW_sgbi_8suYnn4#Ou}%9u)dXd3huFIb!ytlX>Denx@pCS-Nj$`VO&j@(z!kKSP0hE4;YIP#w9ta=3DO$7f*x zc9M4&NK%IrVmZAe=r@skWD`AEWH=g+r|*13Ss$+{c_R!b?>?UaGXlw*8qDmY#xlR= z<0XFbs2t?8i^G~m?b|!Hal^ZjRjt<@a? z%({Gn14b4-a|#uY^=@iiKH+k?~~wTj5K1A&hU z2^9-HTC)7zpoWK|$JXaBL6C z#qSNYtY>65T@Zs&-0cHeu|RX(Pxz6vTITdzJdYippF zC-EB+n4}#lM7`2Ry~SO>FxhKboIAF#Z{1wqxaCb{#yEFhLuX;Rx(Lz%T`Xo1+a2M}7D+@wol2)OJs$TwtRNJ={( zD@#zTUEE}#Fz#&(EoD|SV#bayvr&E0vzmb%H?o~46|FAcx?r4$N z&67W3mdip-T1RIxwSm_&(%U|+WvtGBj*}t69XVd&ebn>KOuL(7Y8cV?THd-(+9>G7*Nt%T zcH;`p={`SOjaf7hNd(=37Lz3-51;58JffzIPgGs_7xIOsB5p2t&@v1mKS$2D$*GQ6 zM(IR*j4{nri7NMK9xlDy-hJW6sW|ZiDRaFiayj%;(%51DN!ZCCCXz+0Vm#};70nOx zJ#yA0P3p^1DED;jGdPbQWo0WATN=&2(QybbVdhd=Vq*liDk`c7iZ?*AKEYC#SY&2g z&Q(Ci)MJ{mEat$ZdSwTjf6h~roanYh2?9j$CF@4hjj_f35kTKuGHvIs9}Re@iKMxS-OI*`0S z6s)fOtz}O$T?PLFVSeOjSO26$@u`e<>k(OSP!&YstH3ANh>)mzmKGNOwOawq-MPXe zy4xbeUAl6tamnx))-`Gi2uV5>9n(73yS)Ukma4*7fI8PaEwa)dWHs6QA6>$}7?(L8 ztN8M}?{Tf!Zu22J5?2@95&rQ|F7=FK-hihT-vDp!5JCcWrVogEnp;CHenAZ)+E+K5 z$Cffk5sNwD_?4+ymgcHR(5xgt20Z8M`2*;MzOM#>yhk{r3x=EyM226wb&!+j`W<%* zSc&|`8!>dn9D@!pYow~(DsY_naSx7(Z4i>cu#hA5=;IuI88}7f%)bRkuY2B;+9Uep zpXcvFWkJ!mQai63BgNXG26$5kyhZ2&*3Q_tk)Ii4M>@p~_~q_cE!|^A;_MHB;7s#9 zKzMzK{lIxotjc};k67^Xsl-gS!^*m*m6kn|sbdun`O?dUkJ{0cmI0-_2y=lTAfn*Y zKg*A-2sJq)CCJgY0LF-VQvl&6HIXZyxo2#!O&6fOhbHXC?%1cMc6y^*dOS{f$=137Ds1m01qs`>iUQ49JijsaQ( zksqV9@&?il$|4Ua%4!O15>Zy&%gBY&wgqB>XA3!EldQ%1CRSM(pp#k~-pkcCg4LAT zXE=puHbgsw)!xtc@P4r~Z}nTF=D2~j(6D%gTBw$(`Fc=OOQ0kiW$_RDd=hcO0t97h zb86S5r=>(@VGy1&#S$Kg_H@7G^;8Ue)X5Y+IWUi`o;mpvoV)`fcVk4FpcT|;EG!;? zHG^zrVVZOm>1KFaHlaogcWj(v!S)O(Aa|Vo?S|P z5|6b{qkH(USa*Z7-y_Uvty_Z1|B{rTS^qmEMLEYUSk03_Fg&!O3BMo{b^*`3SHvl0 zhnLTe^_vVIdcSHe)SQE}r~2dq)VZJ!aSKR?RS<(9lzkYo&dQ?mubnWmgMM37Nudwo z3Vz@R{=m2gENUE3V4NbIzAA$H1z0pagz94-PTJyX{b$yndsdKptmlKQKaaHj@3=ED zc7L?p@%ui|RegVYutK$64q4pe9+5sv34QUpo)u{1ci?)_7gXQd{PL>b0l(LI#rJmN zGuO+%GO`xneFOOr4EU(Wg}_%bhzUf;d@TU+V*2#}!2OLwg~%D;1FAu=Un>OgjPb3S z7l(riiCwgghC=Lm5hWGf5NdGp#01xQ59`HJcLXbUR3&n%P(+W2q$h2Qd z*6+-QXJ*&Kvk9ht0f0*rO_|FMBALen{j7T1l%=Q>gf#kma zQlg#I9+HB+z*5BMxdesMND`_W;q5|FaEURFk|~&{@qY32N$G$2B=&Po{=!)x5b!#n zxLzblkq{yj05#O7(GRuT39(06FJlalyv<#K4m}+vs>9@q-&31@1(QBv82{}Zkns~K ze{eHC_RDX0#^A*JQTwF`a=IkE6Ze@j#-8Q`tTT?k9`^ZhA~3eCZJ-Jr{~7Cx;H4A3 zcZ+Zj{mzFZbVvQ6U~n>$U2ZotGsERZ@}VKrgGh0xM;Jzt29%TX6_&CWzg+YYMozrM z`nutuS)_0dCM8UVaKRj804J4i%z2BA_8A4OJRQ$N(P9Mfn-gF;4#q788C@9XR0O3< zsoS4wIoyt046d+LnSCJOy@B@Uz*#GGd#+Ln1ek5Dv>(ZtD@tgZlPnZZJGBLr^JK+!$$?A_fA3LOrkoDRH&l7 zcMcD$Hsjko3`-{bn)jPL6E9Ds{WskMrivsUu5apD z?grQO@W7i5+%X&E&p|RBaEZ(sGLR@~(y^BI@lDMot^Ll?!`90KT!JXUhYS`ZgX3jnu@Ja^seA*M5R@f`=`ynQV4rc$uT1mvE?@tz)TN<=&H1%Z?5yjxcpO+6y_R z6EPuPKM5uxKpmZfT(WKjRRNHs@ib)F5WAP7QCADvmCSD#hPz$V10wiD&{NXyEwx5S z6NE`3z!IS^$s7m}PCwQutVQ#~w+V z=+~->DI*bR2j0^@dMr9`p>q^Ny~NrAVxrJtX2DUveic5vM%#N*XO|?YAWwNI$Q)_) zvE|L(L1jP@F%gOGtnlXtIv2&1i8q<)Xfz8O3G^Ea~e*HJsQgBxWL(yuLY+jqUK zRE~`-zklrGog(X}$9@ZVUw!8*=l`6mzYLtsg`AvBYz(cxmAhr^j0~(rzXdiOEeu_p zE$sf2(w(BPAvO5DlaN&uQ$4@p-b?fRs}d7&2UQ4Fh?1Hzu*YVjcndqJLw0#q@fR4u zJCJ}>_7-|QbvOfylj+e^_L`5Ep9gqd>XI3-O?Wp z-gt*P29f$Tx(mtS`0d05nHH=gm~Po_^OxxUwV294BDKT>PHVlC5bndncxGR!n(OOm znsNt@Q&N{TLrmsoKFw0&_M9$&+C24`sIXGWgQaz=kY;S{?w`z^Q0JXXBKFLj0w0U6P*+jPKyZHX9F#b0D1$&(- zrm8PJd?+SrVf^JlfTM^qGDK&-p2Kdfg?f>^%>1n8bu&byH(huaocL>l@f%c*QkX2i znl}VZ4R1en4S&Bcqw?$=Zi7ohqB$Jw9x`aM#>pHc0x z0$!q7iFu zZ`tryM70qBI6JWWTF9EjgG@>6SRzsd}3h+4D8d~@CR07P$LJ}MFsYi-*O%XVvD@yT|rJ+Mk zDllJ7$n0V&A!0flbOf)HE6P_afPWZmbhpliqJuw=-h+r;WGk|ntkWN(8tKlYpq5Ow z(@%s>IN8nHRaYb*^d;M(D$zGCv5C|uqmsDjwy4g=Lz>*OhO3z=)VD}C<65;`89Ye} zSCxrv#ILzIpEx1KdLPlM&%Cctf@FqTKvNPXC&`*H9=l=D3r!GLM?UV zOxa(8ZsB`&+76S-_xuj?G#wXBfDY@Z_tMpXJS7^mp z@YX&u0jYw2A+Z+bD#6sgVK5ZgdPSJV3>{K^4~%HV?rn~4D)*2H!67Y>0aOmzup`{D zzDp3c9yEbGCY$U<8biJ_gB*`jluz1ShUd!QUIQJ$*1;MXCMApJ^m*Fiv88RZ zFopLViw}{$Tyhh_{MLGIE2~sZ)t0VvoW%=8qKZ>h=adTe3QM$&$PO2lfqH@brt!9j ziePM8$!CgE9iz6B<6_wyTQj?qYa;eC^{x_0wuwV~W+^fZmFco-o%wsKSnjXFEx02V zF5C2t)T6Gw$Kf^_c;Ei3G~uC8SM-xyycmXyC2hAVi-IfXqhu$$-C=*|X?R0~hu z8`J6TdgflslhrmDZq1f?GXF7*ALeMmOEpRDg(s*H`4>_NAr`2uqF;k;JQ+8>A|_6ZNsNLECC%NNEb1Y1dP zbIEmNpK)#XagtL4R6BC{C5T(+=yA-(Z|Ap}U-AfZM#gwVpus3(gPn}Q$CExObJ5AC z)ff9Yk?wZ}dZ-^)?cbb9Fw#EjqQ8jxF4G3=L?Ra zg_)0QDMV1y^A^>HRI$x?Op@t;oj&H@1xt4SZ9(kifQ zb59B*`M99Td7@aZ3UWvj1rD0sE)d=BsBuW*KwkCds7ay(7*01_+L}b~7)VHI>F_!{ zyxg-&nCO?v#KOUec0{OOKy+sjWA;8rTE|Lv6I9H?CI?H(mUm8VXGwU$49LGpz&{nQp2}dinE1@lZ1iox6{ghN&v^GZv9J${7WaXj)<0S4g_uiJ&JCZ zr8-hsu`U%N;+9N^@&Q0^kVPB3)wY(rr}p7{p0qFHb3NUUHJb672+wRZs`gd1UjKPX z4o6zljKKA+Kkj?H>Ew63o%QjyBk&1!P22;MkD>sM0=z_s-G{mTixJCT9@_|*(p^bz zJ8?ZZ&;pzV+7#6Mn`_U-)k8Pjg?a;|Oe^us^PoPY$Va~yi8|?+&=y$f+lABT<*pZr zP}D{~Pq1Qyni+@|aP;ixO~mbEW9#c0OU#YbDZIaw=_&$K%Ep2f%hO^&P67hApZe`x zv8b`Mz@?M_7-)b!lkQKk)JXXUuT|B8kJlvqRmRpxtQDgvrHMXC1B$M@Y%Me!BSx3P z#2Eawl$HleZhhTS6Txm>lN_+I`>eV$&v9fOg)%zVn3O5mI*lAl>QcHuW6!Kixmq`X zBCZ*Ck6OYtDiK!N47>jxI&O2a9x7M|i^IagRr-fmrmikEQGgw%J7bO|)*$2FW95O4 zeBs>KR)izRG1gRVL;F*sr8A}aRHO0gc$$j&ds8CIO1=Gwq1%_~E)CWNn9pCtBE}+`Jelk4{>S)M)`Ll=!~gnn1yq^EX(+y*ik@3Ou0qU`IgYi3*doM+5&dU!cho$pZ zn%lhKeZkS72P?Cf68<#kll_6OAO26bIbueZx**j6o;I0cS^XiL`y+>{cD}gd%lux} z)3N>MaE24WBZ}s0ApfdM;5J_Ny}rfUyxfkC``Awo2#sgLnGPewK};dORuT?@I6(5~ z?kE)Qh$L&fwJXzK){iYx!l5$Tt|^D~MkGZPA}(o6f7w~O2G6Vvzdo*a;iXzk$B66$ zwF#;wM7A+(;uFG4+UAY(2`*3XXx|V$K8AYu#ECJYSl@S=uZW$ksfC$~qrrbQj4??z-)uz0QL}>k^?fPnJTPw% zGz)~?B4}u0CzOf@l^um}HZzbaIwPmb<)< zi_3@E9lc)Qe2_`*Z^HH;1CXOceL=CHpHS{HySy3T%<^NrWQ}G0i4e1xm_K3(+~oi$ zoHl9wzb?Z4j#90DtURtjtgvi7uw8DzHYmtPb;?%8vb9n@bszT=1qr)V_>R%s!92_` zfnHQPANx z<#hIjIMm#*(v*!OXtF+w8kLu`o?VZ5k7{`vw{Yc^qYclpUGIM_PBN1+c{#Vxv&E*@ zxg=W2W~JuV{IuRYw3>LSI1)a!thID@R=bU+cU@DbR^_SXY`MC7HOsCN z!dO4OKV7(E_Z8T#8MA1H`99?Z!r0)qKW_#|29X3#Jb+5+>qUidbeP1NJ@)(qi2S-X zao|f0_tl(O+$R|Qwd$H{_ig|~I1fbp_$NkI!0E;Y z6JrnU{1Ra6^on{9gUUB0mwzP3S%B#h0fjo>JvV~#+X0P~JV=IG=yHG$O+p5O3NUgG zEQ}z6BTp^Fie)Sg<){Z&I8NwPR(=mO4joTLHkJ>|Tnk23E(Bo`FSbPc05lF2-+)X? z6vV3*m~IBHTy*^E!<0nA(tCOJW2G4DsH7)BxLV8kICn5lu6@U*R`w)o9;Ro$i8=Q^V%uH8n3q=+Yf;SFRZu z!+F&PKcH#8cG?aSK_Tl@K9P#8o+jry@gdexz&d(Q=47<7nw@e@FFfIRNL9^)1i@;A z28+$Z#rjv-wj#heI|<&J_DiJ*s}xd-f!{J8jfqOHE`TiHHZVIA8CjkNQ_u;Ery^^t zl1I75&u^`1_q)crO+JT4rx|z2ToSC>)Or@-D zy3S>jW*sNIZR-EBsfyaJ+Jq4BQE4?SePtD2+jY8*%FsSLZ9MY>+wk?}}}AFAw)vr{ml)8LUG-y9>^t!{~|sgpxYc0Gnkg`&~R z-pilJZjr@y5$>B=VMdZ73svct%##v%wdX~9fz6i3Q-zOKJ9wso+h?VME7}SjL=!NUG{J?M&i!>ma`eoEa@IX`5G>B1(7;%}M*%-# zfhJ(W{y;>MRz!Ic8=S}VaBKqh;~7KdnGEHxcL$kA-6E~=!hrN*zw9N+_=odt<$_H_8dbo;0=42wcAETPCVGUr~v(`Uai zb{=D!Qc!dOEU6v)2eHSZq%5iqK?B(JlCq%T6av$Cb4Rko6onlG&?CqaX7Y_C_cOC3 zYZ;_oI(}=>_07}Oep&Ws7x7-R)cc8zfe!SYxJYP``pi$FDS)4Fvw5HH=FiU6xfVqIM!hJ;Rx8c0cB7~aPtNH(Nmm5Vh{ibAoU#J6 zImRCr?(iyu_4W_6AWo3*vxTPUw@vPwy@E0`(>1Qi=%>5eSIrp^`` zK*Y?fK_6F1W>-7UsB)RPC4>>Ps9)f+^MqM}8AUm@tZ->j%&h1M8s*s!LX5&WxQcAh z8mciQej@RPm?660%>{_D+7er>%zX_{s|$Z+;G7_sfNfBgY(zLB4Ey}J9F>zX#K0f6 z?dVNIeEh?EIShmP6>M+d|0wMM85Sa4diw1hrg|ITJ}JDg@o8y>(rF9mXk5M z2@D|NA)-7>wD&wF;S_$KS=eE84`BGw3g0?6wGxu8ys4rwI?9U=*^VF22t3%mbGeOh z`!O-OpF7#Vceu~F`${bW0nYVU9ecmk31V{tF%iv&5hWofC>I~cqAt@u6|R+|HLMMX zVxuSlMFOK_EQ86#E8&KwxIr8S9tj_goWtLv4f@!&h8;Ov41{J~496vp9vX=(LK#j! zAwi*21RAV-LD>9Cw3bV_9X(X3)Kr0-UaB*7Y>t82EQ%!)(&(XuAYtTsYy-dz+w=$ir)VJpe!_$ z6SGpX^i(af3{o=VlFPC);|J8#(=_8#vdxDe|Cok+ANhYwbE*FO`Su2m1~w+&9<_9~ z-|tTU_ACGN`~CNW5WYYBn^B#SwZ(t4%3aPp z;o)|L6Rk569KGxFLUPx@!6OOa+5OjQLK5w&nAmwxkC5rZ|m&HT8G%GVZxB_@ME z>>{rnXUqyiJrT(8GMj_ap#yN_!9-lO5e8mR3cJiK3NE{_UM&=*vIU`YkiL$1%kf+1 z4=jk@7EEj`u(jy$HnzE33ZVW_J4bj}K;vT?T91YlO(|Y0FU4r+VdbmQ97%(J5 zkK*Bed8+C}FcZ@HIgdCMioV%A<*4pw_n}l*{Cr4}a(lq|injK#O?$tyvyE`S%(1`H z_wwRvk#13ElkZvij2MFGOj`fhy?nC^8`Zyo%yVcUAfEr8x&J#A{|moUBAV_^f$hpaUuyQeY3da^ zS9iRgf87YBwfe}>BO+T&Fl%rfpZh#+AM?Dq-k$Bq`vG6G_b4z%Kbd&v>qFjow*mBl z-OylnqOpLg}or7_VNwRg2za3VBK6FUfFX{|TD z`Wt0Vm2H$vdlRWYQJqDmM?JUbVqL*ZQY|5&sY*?!&%P8qhA~5+Af<{MaGo(dl&C5t zE%t!J0 zh6jqANt4ABdPxSTrVV}fLsRQal*)l&_*rFq(Ez}ClEH6LHv{J#v?+H-BZ2)Wy{K@9 z+ovXHq~DiDvm>O~r$LJo!cOuwL+Oa--6;UFE2q@g3N8Qkw5E>ytz^(&($!O47+i~$ zKM+tkAd-RbmP{s_rh+ugTD;lriL~`Xwkad#;_aM?nQ7L_muEFI}U_4$phjvYgleK~`Fo`;GiC07&Hq1F<%p;9Q;tv5b?*QnR%8DYJH3P>Svmv47Y>*LPZJy8_{9H`g6kQpyZU{oJ`m%&p~D=K#KpfoJ@ zn-3cqmHsdtN!f?~w+(t+I`*7GQA#EQC^lUA9(i6=i1PqSAc|ha91I%X&nXzjYaM{8$s&wEx@aVkQ6M{E2 zfzId#&r(XwUNtPcq4Ngze^+XaJA1EK-%&C9j>^9(secqe{}z>hR5CFNveMsVA)m#S zk)_%SidkY-XmMWlVnQ(mNJ>)ooszQ#vaK;!rPmGKXV7am^_F!Lz>;~{VrIO$;!#30XRhE1QqO_~#+Ux;B_D{Nk=grn z8Y0oR^4RqtcYM)7a%@B(XdbZCOqnX#fD{BQTeLvRHd(irHKq=4*jq34`6@VAQR8WG z^%)@5CXnD_T#f%@-l${>y$tfb>2LPmc{~5A82|16mH)R?&r#KKLs7xpN-D`=&Cm^R zvMA6#Ahr<3X>Q7|-qfTY)}32HkAz$_mibYV!I)u>bmjK`qwBe(>za^0Kt*HnFbSdO z1>+ryKCNxmm^)*$XfiDOF2|{-v3KKB?&!(S_Y=Ht@|ir^hLd978xuI&N{k>?(*f8H z=ClxVJK_%_z1TH0eUwm2J+2To7FK4o+n_na)&#VLn1m;!+CX+~WC+qg1?PA~KdOlC zW)C@pw75_xoe=w7i|r9KGIvQ$+3K?L{7TGHwrQM{dCp=Z*D}3kX7E-@sZnup!BImw z*T#a=+WcTwL78exTgBn|iNE3#EsOorO z*kt)gDzHiPt07fmisA2LWN?AymkdqTgr?=loT7z@d`wnlr6oN}@o|&JX!yPzC*Y8d zu6kWlTzE1)ckyBn+0Y^HMN+GA$wUO_LN6W>mxCo!0?oiQvT`z$jbSEu&{UHRU0E8# z%B^wOc@S!yhMT49Y)ww(Xta^8pmPCe@eI5C*ed96)AX9<>))nKx0(sci8gwob_1}4 z0DIL&vsJ1_s%<@y%U*-eX z5rN&(zef-5G~?@r79oZGW1d!WaTqQn0F6RIOa9tJ=0(kdd{d1{<*tHT#cCvl*i>YY zH+L7jq8xZNcTUBqj(S)ztTU!TM!RQ}In*n&Gn<>(60G7}4%WQL!o>hbJqNDSGwl#H z`4k+twp0cj%PsS+NKaxslAEu9!#U3xT1|_KB6`h=PI0SW`P9GTa7caD1}vKEglV8# zjKZR`pluCW19c2fM&ZG)c3T3Um;ir3y(tSCJ7Agl6|b524dy5El{^EQBG?E61H0XY z`bqg!;zhGhyMFl&(o=JWEJ8n~z)xI}A@C0d2hQGvw7nGv)?POU@(kS1m=%`|+^ika zXl8zjS?xqW$WlO?Ewa;vF~XbybHBor$f<%I&*t$F5fynwZlTGj|IjZtVfGa7l&tK} zW>I<69w(cZLu)QIVG|M2xzW@S+70NinQzk&Y0+3WT*cC)rx~04O-^<{JohU_&HL5XdUKW!uFy|i$FB|EMu0eUyW;gsf`XfIc!Z0V zeK&*hPL}f_cX=@iv>K%S5kL;cl_$v?n(Q9f_cChk8Lq$glT|=e+T*8O4H2n<=NGmn z+2*h+v;kBvF>}&0RDS>)B{1!_*XuE8A$Y=G8w^qGMtfudDBsD5>T5SB;Qo}fSkkiV ze^K^M(UthkwrD!&*tTsu>Dacdj_q`~V%r_twr$(Ct&_dKeeXE?fA&4&yASJWJ*}~- zel=@W)tusynfC_YqH4ll>4Eg`Xjs5F7Tj>tTLz<0N3)X<1px_d2yUY>X~y>>93*$) z5PuNMQLf9Bu?AAGO~a_|J2akO1M*@VYN^VxvP0F$2>;Zb9;d5Yfd8P%oFCCoZE$ z4#N$^J8rxYjUE_6{T%Y>MmWfHgScpuGv59#4u6fpTF%~KB^Ae`t1TD_^Ud#DhL+Dm zbY^VAM#MrAmFj{3-BpVSWph2b_Y6gCnCAombVa|1S@DU)2r9W<> zT5L8BB^er3zxKt1v(y&OYk!^aoQisqU zH(g@_o)D~BufUXcPt!Ydom)e|aW{XiMnes2z&rE?og>7|G+tp7&^;q?Qz5S5^yd$i z8lWr4g5nctBHtigX%0%XzIAB8U|T6&JsC4&^hZBw^*aIcuNO47de?|pGXJ4t}BB`L^d8tD`H`i zqrP8?#J@8T#;{^B!KO6J=@OWKhAerih(phML`(Rg7N1XWf1TN>=Z3Do{l_!d~DND&)O)D>ta20}@Lt77qSnVsA7>)uZAaT9bsB>u&aUQl+7GiY2|dAEg@%Al3i316y;&IhQL^8fw_nwS>f60M_-m+!5)S_6EPM7Y)(Nq^8gL7(3 zOiot`6Wy6%vw~a_H?1hLVzIT^i1;HedHgW9-P#)}Y6vF%C=P70X0Tk^z9Te@kPILI z_(gk!k+0%CG)%!WnBjjw*kAKs_lf#=5HXC00s-}oM-Q1aXYLj)(1d!_a7 z*Gg4Fe6F$*ujVjI|79Z5+Pr`us%zW@ln++2l+0hsngv<{mJ%?OfSo_3HJXOCys{Ug z00*YR-(fv<=&%Q!j%b-_ppA$JsTm^_L4x`$k{VpfLI(FMCap%LFAyq;#ns5bR7V+x zO!o;c5y~DyBPqdVQX)8G^G&jWkBy2|oWTw>)?5u}SAsI$RjT#)lTV&Rf8;>u*qXnb z8F%Xb=7#$m)83z%`E;49)t3fHInhtc#kx4wSLLms!*~Z$V?bTyUGiS&m>1P(952(H zuHdv=;o*{;5#X-uAyon`hP}d#U{uDlV?W?_5UjJvf%11hKwe&(&9_~{W)*y1nR5f_ z!N(R74nNK`y8>B!0Bt_Vr!;nc3W>~RiKtGSBkNlsR#-t^&;$W#)f9tTlZz>n*+Fjz z3zXZ;jf(sTM(oDzJt4FJS*8c&;PLTW(IQDFs_5QPy+7yhi1syPCarvqrHFcf&yTy)^O<1EBx;Ir`5W{TIM>{8w&PB>ro4;YD<5LF^TjTb0!zAP|QijA+1Vg>{Afv^% zmrkc4o6rvBI;Q8rj4*=AZacy*n8B{&G3VJc)so4$XUoie0)vr;qzPZVbb<#Fc=j+8CGBWe$n|3K& z_@%?{l|TzKSlUEO{U{{%Fz_pVDxs7i9H#bnbCw7@4DR=}r_qV!Zo~CvD4ZI*+j3kO zW6_=|S`)(*gM0Z;;}nj`73OigF4p6_NPZQ-Od~e$c_);;4-7sR>+2u$6m$Gf%T{aq zle>e3(*Rt(TPD}03n5)!Ca8Pu!V}m6v0o1;5<1h$*|7z|^(3$Y&;KHKTT}hV056wuF0Xo@mK-52~r=6^SI1NC%c~CC?n>yX6wPTgiWYVz!Sx^atLby9YNn1Rk{g?|pJaxD4|9cUf|V1_I*w zzxK)hRh9%zOl=*$?XUjly5z8?jPMy%vEN)f%T*|WO|bp5NWv@B(K3D6LMl!-6dQg0 zXNE&O>Oyf%K@`ngCvbGPR>HRg5!1IV$_}m@3dWB7x3t&KFyOJn9pxRXCAzFr&%37wXG;z^xaO$ekR=LJG ztIHpY8F5xBP{mtQidqNRoz= z@){+N3(VO5bD+VrmS^YjG@+JO{EOIW)9=F4v_$Ed8rZtHvjpiEp{r^c4F6Ic#ChlC zJX^DtSK+v(YdCW)^EFcs=XP7S>Y!4=xgmv>{S$~@h=xW-G4FF9?I@zYN$e5oF9g$# zb!eVU#J+NjLyX;yb)%SY)xJdvGhsnE*JEkuOVo^k5PyS=o#vq!KD46UTW_%R=Y&0G zFj6bV{`Y6)YoKgqnir2&+sl+i6foAn-**Zd1{_;Zb7Ki=u394C5J{l^H@XN`_6XTKY%X1AgQM6KycJ+= zYO=&t#5oSKB^pYhNdzPgH~aEGW2=ec1O#s-KG z71}LOg@4UEFtp3GY1PBemXpNs6UK-ax*)#$J^pC_me;Z$Je(OqLoh|ZrW*mAMBFn< zHttjwC&fkVfMnQeen8`Rvy^$pNRFVaiEN4Pih*Y3@jo!T0nsClN)pdrr9AYLcZxZ| zJ5Wlj+4q~($hbtuY zVQ7hl>4-+@6g1i`1a)rvtp-;b0>^`Dloy(#{z~ytgv=j4q^Kl}wD>K_Y!l~ zp(_&7sh`vfO(1*MO!B%<6E_bx1)&s+Ae`O)a|X=J9y~XDa@UB`m)`tSG4AUhoM=5& znWoHlA-(z@3n0=l{E)R-p8sB9XkV zZ#D8wietfHL?J5X0%&fGg@MH~(rNS2`GHS4xTo7L$>TPme+Is~!|79=^}QbPF>m%J zFMkGzSndiPO|E~hrhCeo@&Ea{M(ieIgRWMf)E}qeTxT8Q#g-!Lu*x$v8W^M^>?-g= zwMJ$dThI|~M06rG$Sv@C@tWR>_YgaG&!BAbkGggVQa#KdtDB)lMLNVLN|51C@F^y8 zCRvMB^{GO@j=cHfmy}_pCGbP%xb{pNN>? z?7tBz$1^zVaP|uaatYaIN+#xEN4jBzwZ|YI_)p(4CUAz1ZEbDk>J~Y|63SZaak~#0 zoYKruYsWHoOlC1(MhTnsdUOwQfz5p6-D0}4;DO$B;7#M{3lSE^jnTT;ns`>!G%i*F?@pR1JO{QTuD0U+~SlZxcc8~>IB{)@8p`P&+nDxNj`*gh|u?yrv$phpQcW)Us)bi`kT%qLj(fi{dWRZ%Es2!=3mI~UxiW0$-v3vUl?#g{p6eF zMEUAqo5-L0Ar(s{VlR9g=j7+lt!gP!UN2ICMokAZ5(Agd>})#gkA2w|5+<%-CuEP# zqgcM}u@3(QIC^Gx<2dbLj?cFSws_f3e%f4jeR?4M^M3cx1f+Qr6ydQ>n)kz1s##2w zk}UyQc+Z5G-d-1}{WzjkLXgS-2P7auWSJ%pSnD|Uivj5u!xk0 z_^-N9r9o;(rFDt~q1PvE#iJZ_f>J3gcP$)SOqhE~pD2|$=GvpL^d!r z6u=sp-CrMoF7;)}Zd7XO4XihC4ji?>V&(t^?@3Q&t9Mx=qex6C9d%{FE6dvU6%d94 zIE;hJ1J)cCqjv?F``7I*6bc#X)JW2b4f$L^>j{*$R`%5VHFi*+Q$2;nyieduE}qdS{L8y8F08yLs?w}{>8>$3236T-VMh@B zq-nujsb_1aUv_7g#)*rf9h%sFj*^mIcImRV*k~Vmw;%;YH(&ylYpy!&UjUVqqtfG` zox3esju?`unJJA_zKXRJP)rA3nXc$m^{S&-p|v|-0x9LHJm;XIww7C#R$?00l&Yyj z=e}gKUOpsImwW?N)+E(awoF@HyP^EhL+GlNB#k?R<2>95hz!h9sF@U20DHSB3~WMa zk90+858r@-+vWwkawJ)8ougd(i#1m3GLN{iSTylYz$brAsP%=&m$mQQrH$g%3-^VR zE%B`Vi&m8f3T~&myTEK28BDWCVzfWir1I?03;pX))|kY5ClO^+bae z*7E?g=3g7EiisYOrE+lA)2?Ln6q2*HLNpZEWMB|O-JI_oaHZB%CvYB(%=tU= zE*OY%QY58fW#RG5=gm0NR#iMB=EuNF@)%oZJ}nmm=tsJ?eGjia{e{yuU0l3{d^D@)kVDt=1PE)&tf_hHC%0MB znL|CRCPC}SeuVTdf>-QV70`0(EHizc21s^sU>y%hW0t!0&y<7}Wi-wGy>m%(-jsDj zP?mF|>p_K>liZ6ZP(w5(|9Ga%>tLgb$|doDDfkdW>Z z`)>V2XC?NJT26mL^@ zf+IKr27TfM!UbZ@?zRddC7#6ss1sw%CXJ4FWC+t3lHZupzM77m^=9 z&(a?-LxIq}*nvv)y?27lZ{j zifdl9hyJudyP2LpU$-kXctshbJDKS{WfulP5Dk~xU4Le4c#h^(YjJit4#R8_khheS z|8(>2ibaHES4+J|DBM7I#QF5u-*EdN{n=Kt@4Zt?@Tv{JZA{`4 zU#kYOv{#A&gGPwT+$Ud}AXlK3K7hYzo$(fBSFjrP{QQ zeaKg--L&jh$9N}`pu{Bs>?eDFPaWY4|9|foN%}i;3%;@4{dc+iw>m}{3rELqH21G! z`8@;w-zsJ1H(N3%|1B@#ioLOjib)j`EiJqPQVSbPSPVHCj6t5J&(NcWzBrzCiDt{4 zdlPAUKldz%6x5II1H_+jv)(xVL+a;P+-1hv_pM>gMRr%04@k;DTokASSKKhU1Qms| zrWh3a!b(J3n0>-tipg{a?UaKsP7?+|@A+1WPDiQIW1Sf@qDU~M_P65_s}7(gjTn0X zucyEm)o;f8UyshMy&>^SC3I|C6jR*R_GFwGranWZe*I>K+0k}pBuET&M~ z;Odo*ZcT?ZpduHyrf8E%IBFtv;JQ!N_m>!sV6ly$_1D{(&nO~w)G~Y`7sD3#hQk%^ zp}ucDF_$!6DAz*PM8yE(&~;%|=+h(Rn-=1Wykas_-@d&z#=S}rDf`4w(rVlcF&lF! z=1)M3YVz7orwk^BXhslJ8jR);sh^knJW(Qmm(QdSgIAIdlN4Te5KJisifjr?eB{FjAX1a0AB>d?qY4Wx>BZ8&}5K0fA+d{l8 z?^s&l8#j7pR&ijD?0b%;lL9l$P_mi2^*_OL+b}4kuLR$GAf85sOo02?Y#90}CCDiS zZ%rbCw>=H~CBO=C_JVV=xgDe%b4FaEFtuS7Q1##y686r%F6I)s-~2(}PWK|Z8M+Gu zl$y~5@#0Ka%$M<&Cv%L`a8X^@tY&T7<0|(6dNT=EsRe0%kp1Qyq!^43VAKYnr*A5~ zsI%lK1ewqO;0TpLrT9v}!@vJK{QoVa_+N4FYT#h?Y8rS1S&-G+m$FNMP?(8N`MZP zels(*?kK{{^g9DOzkuZXJ2;SrOQsp9T$hwRB1(phw1c7`!Q!by?Q#YsSM#I12RhU{$Q+{xj83axHcftEc$mNJ8_T7A-BQc*k(sZ+~NsO~xAA zxnbb%dam_fZlHvW7fKXrB~F&jS<4FD2FqY?VG?ix*r~MDXCE^WQ|W|WM;gsIA4lQP zJ2hAK@CF*3*VqPr2eeg6GzWFlICi8S>nO>5HvWzyZTE)hlkdC_>pBej*>o0EOHR|) z$?};&I4+_?wvL*g#PJ9)!bc#9BJu1(*RdNEn>#Oxta(VWeM40ola<0aOe2kSS~{^P zDJBd}0L-P#O-CzX*%+$#v;(x%<*SPgAje=F{Zh-@ucd2DA(yC|N_|ocs*|-!H%wEw z@Q!>siv2W;C^^j^59OAX03&}&D*W4EjCvfi(ygcL#~t8XGa#|NPO+*M@Y-)ctFA@I z-p7npT1#5zOLo>7q?aZpCZ=iecn3QYklP;gF0bq@>oyBq94f6C=;Csw3PkZ|5q=(c zfs`aw?II0e(h=|7o&T+hq&m$; zBrE09Twxd9BJ2P+QPN}*OdZ-JZV7%av@OM7v!!NL8R;%WFq*?{9T3{ct@2EKgc8h) zMxoM$SaF#p<`65BwIDfmXG6+OiK0e)`I=!A3E`+K@61f}0e z!2a*FOaDrOe>U`q%K!QN`&=&0C~)CaL3R4VY(NDt{Xz(Xpqru5=r#uQN1L$Je1*dkdqQ*=lofQaN%lO!<5z9ZlHgxt|`THd>2 zsWfU$9=p;yLyJyM^t zS2w9w?Bpto`@H^xJpZDKR1@~^30Il6oFGfk5%g6w*C+VM)+%R@gfIwNprOV5{F^M2 zO?n3DEzpT+EoSV-%OdvZvNF+pDd-ZVZ&d8 zKeIyrrfPN=EcFRCPEDCVflX#3-)Ik_HCkL(ejmY8vzcf-MTA{oHk!R2*36`O68$7J zf}zJC+bbQk--9Xm!u#lgLvx8TXx2J258E5^*IZ(FXMpq$2LUUvhWQPs((z1+2{Op% z?J}9k5^N=z;7ja~zi8a_-exIqWUBJwohe#4QJ`|FF*$C{lM18z^#hX6!5B8KAkLUX ziP=oti-gpV(BsLD{0(3*dw}4JxK23Y7M{BeFPucw!sHpY&l%Ws4pSm`+~V7;bZ%Dx zeI)MK=4vC&5#;2MT7fS?^ch9?2;%<8Jlu-IB&N~gg8t;6S-#C@!NU{`p7M8@2iGc& zg|JPg%@gCoCQ&s6JvDU&`X2S<57f(k8nJ1wvBu{8r?;q3_kpZZ${?|( z+^)UvR33sjSd)aT!UPkA;ylO6{aE3MQa{g%Mcf$1KONcjO@&g5zPHWtzM1rYC{_K> zgQNcs<{&X{OA=cEWw5JGqpr0O>x*Tfak2PE9?FuWtz^DDNI}rwAaT0(bdo-<+SJ6A z&}S%boGMWIS0L}=S>|-#kRX;e^sUsotry(MjE|3_9duvfc|nwF#NHuM-w7ZU!5ei8 z6Mkf>2)WunY2eU@C-Uj-A zG(z0Tz2YoBk>zCz_9-)4a>T46$(~kF+Y{#sA9MWH%5z#zNoz)sdXq7ZR_+`RZ%0(q zC7&GyS_|BGHNFl8Xa%@>iWh%Gr?=J5<(!OEjauj5jyrA-QXBjn0OAhJJ9+v=!LK`` z@g(`^*84Q4jcDL`OA&ZV60djgwG`|bcD*i50O}Q{9_noRg|~?dj%VtKOnyRs$Uzqg z191aWoR^rDX#@iSq0n z?9Sg$WSRPqSeI<}&n1T3!6%Wj@5iw5`*`Btni~G=&;J+4`7g#OQTa>u`{4ZZ(c@s$ zK0y;ySOGD-UTjREKbru{QaS>HjN<2)R%Nn-TZiQ(Twe4p@-saNa3~p{?^V9Nixz@a zykPv~<@lu6-Ng9i$Lrk(xi2Tri3q=RW`BJYOPC;S0Yly%77c727Yj-d1vF!Fuk{Xh z)lMbA69y7*5ufET>P*gXQrxsW+ zz)*MbHZv*eJPEXYE<6g6_M7N%#%mR{#awV3i^PafNv(zyI)&bH?F}2s8_rR(6%!V4SOWlup`TKAb@ee>!9JKPM=&8g#BeYRH9FpFybxBXQI2|g}FGJfJ+ zY-*2hB?o{TVL;Wt_ek;AP5PBqfDR4@Z->_182W z{P@Mc27j6jE*9xG{R$>6_;i=y{qf(c`5w9fa*`rEzX6t!KJ(p1H|>J1pC-2zqWENF zmm=Z5B4u{cY2XYl(PfrInB*~WGWik3@1oRhiMOS|D;acnf-Bs(QCm#wR;@Vf!hOPJ zgjhDCfDj$HcyVLJ=AaTbQ{@vIv14LWWF$=i-BDoC11}V;2V8A`S>_x)vIq44-VB-v z*w-d}$G+Ql?En8j!~ZkCpQ$|cA0|+rrY>tiCeWxkRGPoarxlGU2?7%k#F693RHT24 z-?JsiXlT2PTqZqNb&sSc>$d;O4V@|b6VKSWQb~bUaWn1Cf0+K%`Q&Wc<>mQ>*iEGB zbZ;aYOotBZ{vH3y<0A*L0QVM|#rf*LIsGx(O*-7)r@yyBIzJnBFSKBUSl1e|8lxU* zzFL+YDVVkIuzFWeJ8AbgN&w(4-7zbiaMn{5!JQXu)SELk*CNL+Fro|2v|YO)1l15t zs(0^&EB6DPMyaqvY>=KL>)tEpsn;N5Q#yJj<9}ImL((SqErWN3Q=;tBO~ExTCs9hB z2E$7eN#5wX4<3m^5pdjm#5o>s#eS_Q^P)tm$@SawTqF*1dj_i#)3};JslbLKHXl_N z)Fxzf>FN)EK&Rz&*|6&%Hs-^f{V|+_vL1S;-1K-l$5xiC@}%uDuwHYhmsV?YcOUlk zOYkG5v2+`+UWqpn0aaaqrD3lYdh0*!L`3FAsNKu=Q!vJu?Yc8n|CoYyDo_`r0mPoo z8>XCo$W4>l(==h?2~PoRR*kEe)&IH{1sM41mO#-36`02m#nTX{r*r`Q5rZ2-sE|nA zhnn5T#s#v`52T5|?GNS`%HgS2;R(*|^egNPDzzH_z^W)-Q98~$#YAe)cEZ%vge965AS_am#DK#pjPRr-!^za8>`kksCAUj(Xr*1NW5~e zpypt_eJpD&4_bl_y?G%>^L}=>xAaV>KR6;^aBytqpiHe%!j;&MzI_>Sx7O%F%D*8s zSN}cS^<{iiK)=Ji`FpO#^zY!_|D)qeRNAtgmH)m;qC|mq^j(|hL`7uBz+ULUj37gj zksdbnU+LSVo35riSX_4z{UX=%n&}7s0{WuZYoSfwAP`8aKN9P@%e=~1`~1ASL-z%# zw>DO&ixr}c9%4InGc*_y42bdEk)ZdG7-mTu0bD@_vGAr*NcFoMW;@r?@LUhRI zCUJgHb`O?M3!w)|CPu~ej%fddw20lod?Ufp8Dmt0PbnA0J%KE^2~AIcnKP()025V> zG>noSM3$5Btmc$GZoyP^v1@Poz0FD(6YSTH@aD0}BXva?LphAiSz9f&Y(aDAzBnUh z?d2m``~{z;{}kZJ>a^wYI?ry(V9hIoh;|EFc0*-#*`$T0DRQ1;WsqInG;YPS+I4{g zJGpKk%%Sdc5xBa$Q^_I~(F97eqDO7AN3EN0u)PNBAb+n+ zWBTxQx^;O9o0`=g+Zrt_{lP!sgWZHW?8bLYS$;1a@&7w9rD9|Ge;Gb?sEjFoF9-6v z#!2)t{DMHZ2@0W*fCx;62d#;jouz`R5Y(t{BT=$N4yr^^o$ON8d{PQ=!O zX17^CrdM~7D-;ZrC!||<+FEOxI_WI3CA<35va%4v>gc zEX-@h8esj=a4szW7x{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1* znV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI z##W$P9M{B3c3Si9gw^jlPU-JqD~Cye;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP> zrp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ueg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{ zlB`9HUl-WWCG|<1XANN3JVAkRYvr5U4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvx zK%p23>M&=KTCgR!Ee8c?DAO2_R?B zkaqr6^BSP!8dHXxj%N1l+V$_%vzHjqvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rU zHfcog>kv3UZAEB*g7Er@t6CF8kHDmKTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B zZ+jjWgjJ!043F+&#_;D*mz%Q60=L9Ove|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw- z19qI#oB(RSNydn0t~;tAmK!P-d{b-@@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^8 z2zk8VXx|>#R^JCcWdBCy{0nPmYFOxN55#^-rlqobe0#L6)bi?E?SPymF*a5oDDeSd zO0gx?#KMoOd&G(2O@*W)HgX6y_aa6iMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H z`oa=g0SyiLd~BxAj2~l$zRSDHxvDs;I4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*( ze-417=bO2q{492SWrqDK+L3#ChUHtz*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEX zATx4K*hcO`sY$jk#jN5WD<=C3nvuVsRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_ zl3F^#f_rDu8l}l8qcAz0FFa)EAt32IUy_JLIhU_J^l~FRH&6-ivSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPm zZi-noqS!^Ftb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@ zfFGJtW3r>qV>1Z0r|L>7I3un^gcep$AAWfZHRvB|E*kktY$qQP_$YG60C@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn` zEgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czP zg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-&SFp;!k?uFayytV$8HPwuyELSXOs^27XvK-D zOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2S43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@ zK^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf z9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^&X%=?`6lCy~?`&WSWt z?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6VjA#>1f@EYiS8MRHZphp zMA_5`znM=pzUpBPO)pXGYpQ6gkine{6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ z<1SE2Edkfk9C!0t%}8Yio09^F`YGzpaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8p zT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{eSyybt)m<=zXoA^RALYG-2t zouH|L*BLvmm9cdMmn+KGopyR@4*=&0&4g|FLoreZOhRmh=)R0bg~ zT2(8V_q7~42-zvb)+y959OAv!V$u(O3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+ zMWQoJI_r$HxL5km1#6(e@{lK3Udc~n0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai< z6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF# zMnbr-f55(cTa^q4+#)=s+ThMaV~E`B8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg% zbOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$18Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9Sq zuGh<9<=AO&g6BZte6hn>Qmvv;Rt)*cJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapi zPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wB zxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5o}_(P;=!y-AjFrERh%8la!z6Fn@lR?^E~H12D?8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2 zwG1|5ikb^qHv&9hT8w83+yv&BQXOQyMVJSBL(Ky~p)gU3#%|blG?IR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-} z9?*x{y(`509qhCV*B47f2hLrGl^<@SuRGR!KwHei?!CM10Tq*YDIoBNyRuO*>3FU? zHjipIE#B~y3FSfOsMfj~F9PNr*H?0oHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R% zrq|ic4fzJ#USpTm;X7K+E%xsT_3VHKe?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>Jm ziU#?2^`>arnsl#)*R&nf_%>A+qwl%o{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVD zM8AI6MM2V*^_M^sQ0dmHu11fy^kOqXqzpr?K$`}BKWG`=Es(9&S@K@)ZjA{lj3ea7_MBP zk(|hBFRjHVMN!sNUkrB;(cTP)T97M$0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5 zI7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIo zIZSVls9kFGsTwvr4{T_LidcWtt$u{kJlW7moRaH6+A5hW&;;2O#$oKyEN8kx`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41Uw z`P+tft^E2B$domKT@|nNW`EHwyj>&}K;eDpe z1bNOh=fvIfk`&B61+S8ND<(KC%>y&?>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xo zaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$itm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H z?n6^}l{D``Me90`^o|q!olsF?UX3YSq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfw zR!gX_%AR=L3BFsf8LxI|K^J}deh0ZdV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z z-G6kzA01M?rba+G_mwNMQD1mbVbNTWmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bA zv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$ z8p_}t*XIOehezolNa-a2x0BS})Y9}&*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWK zDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~VCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjMsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3 z-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$)WL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>I zgy8p#i4GN{>#v=pFYUQT(g&b$OeTy-X_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6< znXs{W!bkP|s_YI*Yx%4stI`=ZO45IK6rBs`g7sP40ic}GZ58s?Mc$&i`kq_tfci>N zIHrC0H+Qpam1bNa=(`SRKjixBTtm&e`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_ z%7SUeH6=TrXt3J@js`4iDD0=IoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bUpX9ATD#moByY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOx zXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+pmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X z?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L z*&?(77!-=zvnCVW&kUcZMb6;2!83si518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j( ziTaS4HhQ)ldR=r)_7vYFUr%THE}cPF{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVA zdDZRybv?H|>`9f$AKVjFWJ=wegO7hOOIYCtd?Vj{EYLT*^gl35|HQ`R=ti+ADm{jyQE7K@kdjuqJhWVSks>b^ zxha88-h3s;%3_5b1TqFCPTxVjvuB5U>v=HyZ$?JSk+&I%)M7KE*wOg<)1-Iy)8-K! z^XpIt|0ibmk9RtMmlUd7#Ap3Q!q9N4atQy)TmrhrFhfx1DAN`^vq@Q_SRl|V z#lU<~n67$mT)NvHh`%als+G-)x1`Y%4Bp*6Un5Ri9h=_Db zA-AdP!f>f0m@~>7X#uBM?diI@)Egjuz@jXKvm zJo+==juc9_<;CqeRaU9_Mz@;3e=E4=6TK+c`|uu#pIqhSyNm`G(X)&)B`8q0RBv#> z`gGlw(Q=1Xmf55VHj%C#^1lpc>LY8kfA@|rlC1EA<1#`iuyNO z(=;irt{_&K=i4)^x%;U(Xv<)+o=dczC5H3W~+e|f~{*ucxj@{Yi-cw^MqYr3fN zF5D+~!wd$#al?UfMnz(@K#wn`_5na@rRr8XqN@&M&FGEC@`+OEv}sI1hw>Up0qAWf zL#e4~&oM;TVfjRE+10B_gFlLEP9?Q-dARr3xi6nQqnw>k-S;~b z;!0s2VS4}W8b&pGuK=7im+t(`nz@FnT#VD|!)eQNp-W6)@>aA+j~K*H{$G`y2|QHY z|Hmy+CR@#jWY4~)lr1qBJB_RfHJFfP<}pK5(#ZZGSqcpyS&}01LnTWk5fzmXMGHkJ zTP6L^B+uj;lmB_W<~4=${+v0>z31M!-_O@o-O9GyW)j_mjx}!0@br_LE-7SIuPP84 z;5=O(U*g_um0tyG|61N@d9lEuOeiRd+#NY^{nd5;-CVlw&Ap7J?qwM^?E29wvS}2d zbzar4Fz&RSR(-|s!Z6+za&Z zY#D<5q_JUktIzvL0)yq_kLWG6DO{ri=?c!y!f(Dk%G{8)k`Gym%j#!OgXVDD3;$&v@qy#ISJfp=Vm>pls@9-mapVQChAHHd-x+OGx)(*Yr zC1qDUTZ6mM(b_hi!TuFF2k#8uI2;kD70AQ&di$L*4P*Y-@p`jdm%_c3f)XhYD^6M8&#Y$ZpzQMcR|6nsH>b=*R_Von!$BTRj7yGCXokoAQ z&ANvx0-Epw`QIEPgI(^cS2f(Y85yV@ygI{ewyv5Frng)e}KCZF7JbR(&W618_dcEh(#+^zZFY;o<815<5sOHQdeax9_!PyM&;{P zkBa5xymca0#)c#tke@3KNEM8a_mT&1gm;p&&JlMGH(cL(b)BckgMQ^9&vRwj!~3@l zY?L5}=Jzr080OGKb|y`ee(+`flQg|!lo6>=H)X4`$Gz~hLmu2a%kYW_Uu8x09Pa0J zKZ`E$BKJ=2GPj_3l*TEcZ*uYRr<*J^#5pILTT;k_cgto1ZL-%slyc16J~OH-(RgDA z%;EjEnoUkZ&acS{Q8`{i6T5^nywgqQI5bDIymoa7CSZG|WWVk>GM9)zy*bNih|QIm z%0+(Nnc*a_xo;$=!HQYaapLms>J1ToyjtFByY`C2H1wT#178#4+|{H0BBqtCdd$L% z_3Hc60j@{t9~MjM@LBalR&6@>B;9?r<7J~F+WXyYu*y3?px*=8MAK@EA+jRX8{CG?GI-< z54?Dc9CAh>QTAvyOEm0^+x;r2BWX|{3$Y7)L5l*qVE*y0`7J>l2wCmW zL1?|a`pJ-l{fb_N;R(Z9UMiSj6pQjOvQ^%DvhIJF!+Th7jO2~1f1N+(-TyCFYQZYw z4)>7caf^Ki_KJ^Zx2JUb z&$3zJy!*+rCV4%jqwyuNY3j1ZEiltS0xTzd+=itTb;IPYpaf?8Y+RSdVdpacB(bVQ zC(JupLfFp8y43%PMj2}T|VS@%LVp>hv4Y!RPMF?pp8U_$xCJ)S zQx!69>bphNTIb9yn*_yfj{N%bY)t{L1cs8<8|!f$;UQ*}IN=2<6lA;x^(`8t?;+ST zh)z4qeYYgZkIy{$4x28O-pugO&gauRh3;lti9)9Pvw+^)0!h~%m&8Q!AKX%urEMnl z?yEz?g#ODn$UM`+Q#$Q!6|zsq_`dLO5YK-6bJM6ya>}H+vnW^h?o$z;V&wvuM$dR& zeEq;uUUh$XR`TWeC$$c&Jjau2it3#%J-y}Qm>nW*s?En?R&6w@sDXMEr#8~$=b(gk zwDC3)NtAP;M2BW_lL^5ShpK$D%@|BnD{=!Tq)o(5@z3i7Z){} zGr}Exom_qDO{kAVkZ*MbLNHE666Kina#D{&>Jy%~w7yX$oj;cYCd^p9zy z8*+wgSEcj$4{WxKmCF(5o7U4jqwEvO&dm1H#7z}%VXAbW&W24v-tS6N3}qrm1OnE)fUkoE8yMMn9S$?IswS88tQWm4#Oid#ckgr6 zRtHm!mfNl-`d>O*1~d7%;~n+{Rph6BBy^95zqI{K((E!iFQ+h*C3EsbxNo_aRm5gj zKYug($r*Q#W9`p%Bf{bi6;IY0v`pB^^qu)gbg9QHQ7 zWBj(a1YSu)~2RK8Pi#C>{DMlrqFb9e_RehEHyI{n?e3vL_}L>kYJC z_ly$$)zFi*SFyNrnOt(B*7E$??s67EO%DgoZL2XNk8iVx~X_)o++4oaK1M|ou73vA0K^503j@uuVmLcHH4ya-kOIDfM%5%(E z+Xpt~#7y2!KB&)PoyCA+$~DXqxPxxALy!g-O?<9+9KTk4Pgq4AIdUkl`1<1#j^cJg zgU3`0hkHj_jxV>`Y~%LAZl^3o0}`Sm@iw7kwff{M%VwtN)|~!p{AsfA6vB5UolF~d zHWS%*uBDt<9y!9v2Xe|au&1j&iR1HXCdyCjxSgG*L{wmTD4(NQ=mFjpa~xooc6kju z`~+d{j7$h-;HAB04H!Zscu^hZffL#9!p$)9>sRI|Yovm)g@F>ZnosF2EgkU3ln0bR zTA}|+E(tt)!SG)-bEJi_0m{l+(cAz^pi}`9=~n?y&;2eG;d9{M6nj>BHGn(KA2n|O zt}$=FPq!j`p&kQ8>cirSzkU0c08%8{^Qyqi-w2LoO8)^E7;;I1;HQ6B$u0nNaX2CY zSmfi)F`m94zL8>#zu;8|{aBui@RzRKBlP1&mfFxEC@%cjl?NBs`cr^nm){>;$g?rhKr$AO&6qV_Wbn^}5tfFBry^e1`%du2~o zs$~dN;S_#%iwwA_QvmMjh%Qo?0?rR~6liyN5Xmej8(*V9ym*T`xAhHih-v$7U}8=dfXi2i*aAB!xM(Xekg*ix@r|ymDw*{*s0?dlVys2e)z62u1 z+k3esbJE=-P5S$&KdFp+2H7_2e=}OKDrf( z9-207?6$@f4m4B+9E*e((Y89!q?zH|mz_vM>kp*HGXldO0Hg#!EtFhRuOm$u8e~a9 z5(roy7m$Kh+zjW6@zw{&20u?1f2uP&boD}$#Zy)4o&T;vyBoqFiF2t;*g=|1=)PxB z8eM3Mp=l_obbc?I^xyLz?4Y1YDWPa+nm;O<$Cn;@ane616`J9OO2r=rZr{I_Kizyc zP#^^WCdIEp*()rRT+*YZK>V@^Zs=ht32x>Kwe zab)@ZEffz;VM4{XA6e421^h~`ji5r%)B{wZu#hD}f3$y@L0JV9f3g{-RK!A?vBUA}${YF(vO4)@`6f1 z-A|}e#LN{)(eXloDnX4Vs7eH|<@{r#LodP@Nz--$Dg_Par%DCpu2>2jUnqy~|J?eZ zBG4FVsz_A+ibdwv>mLp>P!(t}E>$JGaK$R~;fb{O3($y1ssQQo|5M;^JqC?7qe|hg zu0ZOqeFcp?qVn&Qu7FQJ4hcFi&|nR!*j)MF#b}QO^lN%5)4p*D^H+B){n8%VPUzi! zDihoGcP71a6!ab`l^hK&*dYrVYzJ0)#}xVrp!e;lI!+x+bfCN0KXwUAPU9@#l7@0& QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/examples/benchmarking/gradle/wrapper/gradle-wrapper.properties b/examples/benchmarking/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..69a97150 --- /dev/null +++ b/examples/benchmarking/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/benchmarking/gradlew b/examples/benchmarking/gradlew new file mode 100755 index 00000000..744e882e --- /dev/null +++ b/examples/benchmarking/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/benchmarking/gradlew.bat b/examples/benchmarking/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/examples/benchmarking/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/benchmarking/settings.gradle b/examples/benchmarking/settings.gradle new file mode 100644 index 00000000..9efe9f92 --- /dev/null +++ b/examples/benchmarking/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'benchmarking' + diff --git a/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java b/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java new file mode 100644 index 00000000..0683c9af --- /dev/null +++ b/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics; + +import com.snowplowanalytics.snowplow.tracker.Tracker; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; +import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; +import com.snowplowanalytics.snowplow.tracker.events.PageView; +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; +import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 15, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 20, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(5) +public class TrackerBenchmark { + public static class MockHttpClientAdapter implements HttpClientAdapter { + @Override + public int post(SelfDescribingJson payload) { + return 200; + } + + @Override + public int get(TrackerPayload payload) { + return 0; + } + + @Override + public String getUrl() { + return null; + } + + @Override + public Object getHttpClient() { + return null; + } + } + + public static BatchEmitter getEmitter() { + MockHttpClientAdapter mockHttpClientAdapter = new MockHttpClientAdapter(); + return BatchEmitter.builder() + .httpClientAdapter(mockHttpClientAdapter) + .build(); + } + + public static Tracker getTracker(Emitter emitter) { + return new Tracker.TrackerBuilder(emitter, "namespace", "appId").build(); + } + + public static void closeThreads(Tracker tracker) { + // Use this line for versions 0.12.0 onwards +// tracker.close(); + // Use these lines for previous versions + BatchEmitter emitter = (BatchEmitter) tracker.getEmitter(); + emitter.close(); + } + + // This State class exists only to print out the tracker version + @State(Scope.Benchmark) + public static class TrackerVersion { + BatchEmitter emitter = getEmitter(); + Tracker tracker = getTracker(emitter); + + @Setup(Level.Trial) + public void printTrackerVersion() { + System.out.println("Using tracker version: " + tracker.getTrackerVersion()); + } + + @TearDown(Level.Trial) + public void doTearDown() { + System.out.println("Do TearDown for trackerVersion state"); + closeThreads(tracker); + } + } + + // This class creates the tracker components. + // They are recreated for every iteration of the benchmark test. + @State(Scope.Benchmark) + public static class TrackerComponents { + Tracker tracker; + BatchEmitter emitter; + + PageView pageViewEvent = PageView.builder() + .pageUrl("url") + .pageTitle("title") + .referrer("referrer") + .build(); + + @Setup(Level.Iteration) + public void doSetUp() { + emitter = getEmitter(); + tracker = getTracker(emitter); + } + + @TearDown(Level.Iteration) + public void doTearDown() { + closeThreads(tracker); + } + } + + // The Blackhole forces JMH to measure the method. + @Benchmark + public void testTrackEvent(Blackhole blackhole, TrackerComponents trackerComponents, TrackerVersion trackerVersion) { + trackerComponents.tracker.track(trackerComponents.pageViewEvent); + blackhole.consume(trackerComponents); + } +} From f7797ad513f476af59c2820d40d8b9de8e673bda Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 24 Feb 2022 10:07:03 +0000 Subject: [PATCH 075/128] Rename bufferSize to batchSize (close #306) * Rename bufferSize to batchSize (close #306) * Remove comment from simple-console demo --- .../main/java/com/snowplowanalytics/Main.java | 4 +- .../tracker/emitter/AbstractEmitter.java | 12 +++--- .../tracker/emitter/BatchEmitter.java | 38 +++++++++---------- .../snowplow/tracker/emitter/Emitter.java | 17 ++++----- .../tracker/emitter/SimpleEmitter.java | 16 ++++---- .../snowplow/tracker/TrackerTest.java | 4 +- .../tracker/emitter/BatchEmitterTest.java | 14 +++---- 7 files changed, 51 insertions(+), 54 deletions(-) diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index ba693590..4ea1f85d 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -45,7 +45,7 @@ public static void main(String[] args) { // build an emitter, this is used by the tracker to batch and schedule transmission of events BatchEmitter emitter = BatchEmitter.builder() .url(collectorEndpoint) - .bufferSize(4) // send batches of 4 events. In production this number should be higher, depending on the size/event volume + .batchSize(4) // send batches of 4 events. In production this number should be higher, depending on the size/event volume .build(); // now we have the emitter, we need a tracker to turn our events into something a Snowplow collector can understand @@ -154,7 +154,7 @@ public static void main(String[] args) { tracker.track(structured); // Will close all threads and force send remaining events - tracker.close(); + emitter.close(); System.out.println("Tracked 7 events"); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index 6b3075a1..ccf6b551 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -134,13 +134,13 @@ protected AbstractEmitter(final Builder builder) { public abstract void add(TrackerPayload payload); /** - * Customize the emitter buffer size to any valid integer greater than zero. + * Customize the emitter batch size to any valid integer greater than zero. * Has no effect on SimpleEmitter * - * @param bufferSize number of events to collect before sending + * @param batchSize number of events to collect before sending */ @Override - public abstract void setBufferSize(final int bufferSize); + public abstract void setBatchSize(final int batchSize); /** * Removes all payloads from the buffer and sends them @@ -149,12 +149,12 @@ protected AbstractEmitter(final Builder builder) { public abstract void flushBuffer(); /** - * Gets the Emitter Buffer Size - Will always be 1 for SimpleEmitter + * Gets the Emitter Batch Size - Will always be 1 for SimpleEmitter * - * @return the buffer size + * @return the batch size */ @Override - public abstract int getBufferSize(); + public abstract int getBatchSize(); /** * Returns List of Payloads that are in the buffer. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 4beb379c..888adf19 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -41,22 +41,22 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private final Thread checkForEventsToSend; private boolean isClosing = false; - private int bufferSize = 1; + private int batchSize; private final EventStore eventStore; private final long closeTimeout = 5; public static abstract class Builder> extends AbstractEmitter.Builder { - private int bufferSize = 50; // Optional + private int batchSize = 50; // Optional private EventStore eventStore = new InMemoryEventStore(); /** - * @param bufferSize The count of events to buffer before sending + * @param batchSize The count of events to buffer before sending * @return itself */ - public T bufferSize(final int bufferSize) { - this.bufferSize = bufferSize; + public T batchSize(final int batchSize) { + this.batchSize = batchSize; return self(); } @@ -85,9 +85,9 @@ protected BatchEmitter(final Builder builder) { super(builder); // Precondition checks - Preconditions.checkArgument(builder.bufferSize > 0, "bufferSize must be greater than 0"); + Preconditions.checkArgument(builder.batchSize > 0, "batchSize must be greater than 0"); - this.bufferSize = builder.bufferSize; + this.batchSize = builder.batchSize; this.eventStore = builder.eventStore; checkForEventsToSend = new Thread( @@ -130,36 +130,36 @@ public List getBuffer() { } /** - * Customize the emitter buffer size to any valid integer greater than zero. + * Customize the emitter batch size to any valid integer greater than zero. * - * @param bufferSize number of events to collect before sending + * @param batchSize number of events to collect before sending */ @Override - public void setBufferSize(final int bufferSize) { - Preconditions.checkArgument(bufferSize > 0, "bufferSize must be greater than 0"); - this.bufferSize = bufferSize; + public void setBatchSize(final int batchSize) { + Preconditions.checkArgument(batchSize > 0, "batchSize must be greater than 0"); + this.batchSize = batchSize; } /** - * Gets the Emitter Buffer Size + * Gets the Emitter batch Size * - * @return the buffer size + * @return the batch size */ @Override - public int getBufferSize() { - return this.bufferSize; + public int getBatchSize() { + return this.batchSize; } /** - * Checks if bufferSize is reached + * Checks if batchSize is reached * * @return the new Runnable object */ private Runnable getCheckForEventsToSendRunnable() { return () -> { while (!isClosing) { - if (eventStore.getSize() >= bufferSize) { - drainEventsAndSend(getBufferSize()); + if (eventStore.getSize() >= batchSize) { + drainEventsAndSend(this.getBatchSize()); } } }; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index fddc0c56..aac70315 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -30,30 +30,27 @@ public interface Emitter { void add(TrackerPayload payload); /** - * Customize the emitter buffer size to any valid integer + * Customize the emitter batch size to any valid integer * greater than zero. * - Will only affect the BatchEmitter * - * @param bufferSize number of events to collect before + * @param batchSize number of events to collect before * sending */ - void setBufferSize(int bufferSize); + void setBatchSize(int batchSize); /** - * When the buffer limit is reached sending of the buffer is - * initiated. - * - * This can be used to manually start sending. + * This can be used to manually send all buffered events. */ void flushBuffer(); /** - * Gets the Emitter Buffer Size + * Gets the Emitter Batch Size * - Will always be 1 for SimpleEmitter * - * @return the buffer size + * @return the batch size */ - int getBufferSize(); + int getBatchSize(); /** * Returns the List of Payloads that are in the buffer. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index e3849efb..14d49327 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -100,25 +100,25 @@ public List getBuffer() { } /** - * Customize the emitter buffer size to any valid integer greater than zero. + * Customize the emitter batch size to any valid integer greater than zero. * Has no effect on SimpleEmitter * - * @param bufferSize number of events to collect before sending + * @param batchSize number of events to collect before sending */ @Override - public void setBufferSize(final int bufferSize) { - if (bufferSize != 1) { - LOGGER.debug("Noop. SimpleEmitter buffer size must always be 1."); + public void setBatchSize(final int batchSize) { + if (batchSize != 1) { + LOGGER.debug("Noop. SimpleEmitter batch size must always be 1."); } } /** - * Gets the Emitter Buffer Size - Will always be 1 for SimpleEmitter + * Gets the Emitter batch Size - Will always be 1 for SimpleEmitter * - * @return the buffer size + * @return the batch size */ @Override - public int getBufferSize() { + public int getBatchSize() { return 1; } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 7e1ce57b..533c0385 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -41,13 +41,13 @@ public void add(TrackerPayload payload) { } @Override - public void setBufferSize(int bufferSize) {} + public void setBatchSize(int batchSize) {} @Override public void flushBuffer() {} @Override - public int getBufferSize() { + public int getBatchSize() { return 0; } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 47d69843..05c570c6 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -71,7 +71,7 @@ public void setUp() { mockHttpClientAdapter = new MockHttpClientAdapter(); emitter = BatchEmitter.builder() .httpClientAdapter(mockHttpClientAdapter) - .bufferSize(10) + .batchSize(10) .build(); } @@ -125,15 +125,15 @@ public void flushBuffer_shouldEmptyBuffer() throws InterruptedException { } @Test - public void setBufferSize_WithNegativeValue_ThrowInvalidArgumentException() { - Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> emitter.setBufferSize(-1)); - Assert.assertEquals("bufferSize must be greater than 0", exception.getMessage()); + public void setBatchSize_WithNegativeValue_ThrowInvalidArgumentException() { + Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> emitter.setBatchSize(-1)); + Assert.assertEquals("batchSize must be greater than 0", exception.getMessage()); } @Test - public void setAndGetBufferSizeWorksAsExpected() throws InterruptedException { - emitter.setBufferSize(2); - Assert.assertEquals(2, emitter.getBufferSize()); + public void setAndGetBatchSizeWorksAsExpected() throws InterruptedException { + emitter.setBatchSize(2); + Assert.assertEquals(2, emitter.getBatchSize()); List payloads = createPayloads(2); for (TrackerPayload payload : payloads) { From bd00fd3e887546e06a74cf037e53c79a73803d8f Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 3 Mar 2022 11:35:26 +0000 Subject: [PATCH 076/128] Add retry to in-memory storage system (close #156) * Refactor InMemoryEventStore * Add a hashmap to eventStore for events currently being sent * Add basic retry * Add benchmarking test for InMemoryEventStore design * Use LinkedBlockingDeque in InMemoryEventStore * Alter simple-console for throughput test * Remove Tracker threadpool * Use scheduled request Runnable to add retry backoff time * Enclose Emitter request method in try catch block * Allow event buffer max capacity to be configured * Fix simple-console demo * Remove unused benchmark class * Restore SimpleEmitter functionality * Use atomicLong more effectively * Remove unnecessary 'this'es * Tidy up Emitter code * Update BatchEmitter javadoc comments * Remove implNote javadoc tags * Tidy up tests * Rename EventStore getEventBatch to getEventsBatch --- build.gradle | 5 +- examples/benchmarking/build.gradle | 1 - .../snowplowanalytics/TrackerBenchmark.java | 3 - .../main/java/com/snowplowanalytics/Main.java | 5 +- .../snowplow/tracker/Tracker.java | 128 ++-------------- .../tracker/emitter/AbstractEmitter.java | 25 ++- .../tracker/emitter/BatchEmitter.java | 137 ++++++++++------- .../tracker/emitter/BatchPayload.java | 37 +++++ .../snowplow/tracker/emitter/EventStore.java | 10 +- .../tracker/emitter/InMemoryEventStore.java | 69 +++++++-- .../tracker/emitter/SimpleEmitter.java | 7 +- .../tracker/events/AbstractEvent.java | 2 +- .../snowplow/tracker/TrackerTest.java | 30 +--- .../tracker/emitter/BatchEmitterTest.java | 145 +++++++++++++++--- .../emitter/InMemoryEventStoreTest.java | 96 +++++++----- 15 files changed, 398 insertions(+), 302 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java diff --git a/build.gradle b/build.gradle index 15ba1289..639091a3 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.12.0-alpha.0' +version = '0.12.0-alpha.1' sourceCompatibility = '1.8' targetCompatibility = '1.8' @@ -80,7 +80,8 @@ dependencies { // Testing libraries testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testCompileOnly 'junit:junit:4.13' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' diff --git a/examples/benchmarking/build.gradle b/examples/benchmarking/build.gradle index f5c3f773..e00d4ad9 100644 --- a/examples/benchmarking/build.gradle +++ b/examples/benchmarking/build.gradle @@ -34,4 +34,3 @@ repositories { dependencies { jmh 'com.snowplowanalytics:snowplow-java-tracker:0.10.1' } - diff --git a/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java b/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java index 0683c9af..c51b900f 100644 --- a/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java +++ b/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java @@ -66,9 +66,6 @@ public static Tracker getTracker(Emitter emitter) { } public static void closeThreads(Tracker tracker) { - // Use this line for versions 0.12.0 onwards -// tracker.close(); - // Use these lines for previous versions BatchEmitter emitter = (BatchEmitter) tracker.getEmitter(); emitter.close(); } diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 4ea1f85d..490c30dd 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -18,12 +18,14 @@ import com.snowplowanalytics.snowplow.tracker.Tracker; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.events.*; +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import java.util.List; import static java.util.Collections.singletonList; import com.google.common.collect.ImmutableMap; +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; public class Main { @@ -34,7 +36,7 @@ public static String getUrlFromArgs(String[] args) { return args[0]; } - public static void main(String[] args) { + public static void main(String[] args) throws InterruptedException { String collectorEndpoint = getUrlFromArgs(args); // the application id to attach to events @@ -155,6 +157,7 @@ public static void main(String[] args) { // Will close all threads and force send remaining events emitter.close(); + Thread.sleep(5000); System.out.println("Tracked 7 events"); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index ae92a5eb..8079ab9b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -16,7 +16,6 @@ import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; -import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -26,18 +25,12 @@ import org.slf4j.LoggerFactory; import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; public class Tracker { private Emitter emitter; private Subject subject; private final TrackerParameters parameters; - protected ExecutorService executor; private static final Logger LOGGER = LoggerFactory.getLogger(Tracker.class); /** @@ -58,11 +51,6 @@ private Tracker(TrackerBuilder builder) { this.emitter = builder.emitter; this.subject = builder.subject; - if (builder.requestExecutorService != null) { - this.executor = builder.requestExecutorService; - } else { - this.executor = Executors.newScheduledThreadPool(builder.threadCount, new TrackerThreadFactory()); - } } /** @@ -76,8 +64,6 @@ public static class TrackerBuilder { private Subject subject = null; // Optional private DevicePlatform platform = DevicePlatform.ServerSideApp; // Optional private boolean base64Encoded = true; // Optional - private int threadCount = 50; // Optional - private ExecutorService requestExecutorService = null; // Optional /** * @param emitter Emitter to which events will be sent @@ -117,30 +103,6 @@ public TrackerBuilder base64(Boolean base64) { return this; } - /** - * Sets the Thread Count for the ExecutorService - * - * @param threadCount the size of the thread pool - * @return itself - */ - public TrackerBuilder threadCount(final int threadCount) { - this.threadCount = threadCount; - return this; - } - - /** - * Set a custom ExecutorService to send http request. - * - * @param executorService the ExecutorService to use - * @return itself - */ - public TrackerBuilder requestExecutorService(final ExecutorService executorService) { - this.requestExecutorService = executorService; - return this; - } - - - /** * Creates a new Tracker * @@ -230,45 +192,6 @@ public TrackerParameters getParameters() { // --- Event Tracking Functions - /** - * Sends a runnable to the executor service. - * - * @param runnable the runnable to be queued - */ - protected void execute(final Runnable runnable) { - this.executor.execute(runnable); - } - - /** - * Copied from `Executors.defaultThreadFactory()`. - * The only change is the generated name prefix. - */ - static class TrackerThreadFactory implements ThreadFactory { - private static final AtomicInteger poolNumber = new AtomicInteger(1); - private final ThreadGroup group; - private final AtomicInteger threadNumber = new AtomicInteger(1); - private final String namePrefix; - - TrackerThreadFactory() { - SecurityManager securityManager = System.getSecurityManager(); - this.group = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); - this.namePrefix = "snowplow-tracker-pool-" + poolNumber.getAndIncrement() + "-event-thread-"; - } - - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(this.group, runnable, this.namePrefix + this.threadNumber.getAndIncrement(), 0L); - if (thread.isDaemon()) { - thread.setDaemon(false); - } - - if (thread.getPriority() != 5) { - thread.setPriority(5); - } - - return thread; - } - } - /** * Handles tracking the different types of events that * the Tracker can encounter. @@ -276,23 +199,17 @@ public Thread newThread(Runnable runnable) { * @param event the event to track */ public void track(Event event) { - execute(getProcessEventRunnable(event)); - } - - private Runnable getProcessEventRunnable(Event event) { - return () -> { - // a list because Ecommerce events become multiple Payloads - List processedEvents = eventTypeSpecificPreProcessing(event); - for (Event processedEvent : processedEvents) { - // Event ID (eid) and device_created_timestamp (dtm) are added during getPayload() - TrackerPayload payload = (TrackerPayload) processedEvent.getPayload(); - - addTrackerParameters(payload); - addContext(processedEvent, payload); - addSubject(processedEvent, payload); - this.emitter.add(payload); - } - }; + // a list because Ecommerce events become multiple Payloads + List processedEvents = eventTypeSpecificPreProcessing(event); + for (Event processedEvent : processedEvents) { + // Event ID (eid) and device_created_timestamp (dtm) are generated when the Event is initialised + TrackerPayload payload = (TrackerPayload) processedEvent.getPayload(); + + addTrackerParameters(payload); + addContext(processedEvent, payload); + addSubject(processedEvent, payload); + this.emitter.add(payload); + } } private List eventTypeSpecificPreProcessing(Event event) { @@ -381,27 +298,4 @@ private void addSubject(Event event, TrackerPayload payload) { } } - public void close() { - // Shutdown executor thread pool for the tracker - if (executor != null) { - executor.shutdown(); - try { - if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { - executor.shutdownNow(); - if (!executor.awaitTermination(1, TimeUnit.SECONDS)) - LOGGER.warn("Tracker executor did not terminate"); - } - } catch (InterruptedException ie) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - // Shutdown executor thread pool for the emitter - if (this.emitter.getClass().equals(BatchEmitter.class)) { - BatchEmitter emitter = (BatchEmitter) this.emitter; - emitter.close(); - } - } - } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index ccf6b551..e56e4c14 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -13,8 +13,8 @@ package com.snowplowanalytics.snowplow.tracker.emitter; import java.util.List; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; @@ -33,24 +33,26 @@ public abstract class AbstractEmitter implements Emitter { protected HttpClientAdapter httpClientAdapter; - protected ExecutorService executor; + protected ScheduledExecutorService executor; public static abstract class Builder> { private HttpClientAdapter httpClientAdapter; // Optional private int threadCount = 50; // Optional - private ExecutorService requestExecutorService = null; // Optional + private ScheduledExecutorService requestExecutorService = null; // Optional private String collectorUrl = null; // Required if not specifying a httpClientAdapter protected abstract T self(); /** - * Set a custom ExecutorService to send http request. + * Set a custom ScheduledExecutorService to send http request. + *

+ * Implementation note: Be aware that calling `close()` on a BatchEmitter instance + * has a side-effect and will shutdown that ExecutorService. * - * /!\ Be aware that calling `close()` on a BatchEmitter instance has a side-effect and will shutdown that ExecutorService. - * @param executorService the ExecutorService to use + * @param executorService the ScheduledExecutorService to use * @return itself */ - public T requestExecutorService(final ExecutorService executorService) { + public T requestExecutorService(final ScheduledExecutorService executorService) { this.requestExecutorService = executorService; return self(); } @@ -164,15 +166,6 @@ protected AbstractEmitter(final Builder builder) { @Override public abstract List getBuffer(); - /** - * Sends a runnable to the executor service. - * - * @param runnable the runnable to be queued - */ - protected void execute(final Runnable runnable) { - this.executor.execute(runnable); - } - /** * Checks whether the response code was a success or not. * diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 888adf19..c76951eb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.constants.Constants; @@ -35,21 +35,17 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); - private static final AtomicInteger EVENTS_CHECK_THREAD_NUMBER = new AtomicInteger(1); - private static final String EVENTS_CHECK_THREAD_NAME_PREFIX = "snowplow-emitter-checkForEvents-thread-"; - - private final Thread checkForEventsToSend; private boolean isClosing = false; - + private int batchSize; private final EventStore eventStore; - - private final long closeTimeout = 5; + private final AtomicLong retryDelay; public static abstract class Builder> extends AbstractEmitter.Builder { private int batchSize = 50; // Optional - private EventStore eventStore = new InMemoryEventStore(); + private int bufferCapacity = Integer.MAX_VALUE; + private EventStore eventStore; /** * @param batchSize The count of events to buffer before sending @@ -60,11 +56,24 @@ public T batchSize(final int batchSize) { return self(); } + /** + * @param eventStore The EventStore to use + * @return itself + */ public T eventStore(final EventStore eventStore) { this.eventStore = eventStore; return self(); } + /** + * @param bufferCapacity The maximum capacity of the default InMemoryEventStore event buffer + * @return itself + */ + public T bufferCapacity(final int bufferCapacity) { + this.bufferCapacity = bufferCapacity; + return self(); + } + public BatchEmitter build() { return new BatchEmitter(this); } @@ -86,37 +95,45 @@ protected BatchEmitter(final Builder builder) { // Precondition checks Preconditions.checkArgument(builder.batchSize > 0, "batchSize must be greater than 0"); + batchSize = builder.batchSize; - this.batchSize = builder.batchSize; - this.eventStore = builder.eventStore; - - checkForEventsToSend = new Thread( - getCheckForEventsToSendRunnable(), - EVENTS_CHECK_THREAD_NAME_PREFIX + EVENTS_CHECK_THREAD_NUMBER.getAndIncrement() - ); - checkForEventsToSend.start(); + if (builder.eventStore == null) { + eventStore = new InMemoryEventStore(builder.bufferCapacity); + } else { + eventStore = builder.eventStore; + } + retryDelay = new AtomicLong(0L); } /** * Adds a TrackerPayload to the concurrent queue buffer + *

+ * Implementation note: Be aware that calling `close()` on a BatchEmitter instance + * has a side-effect and will shutdown that ExecutorService. * * @param payload a payload */ @Override public void add(final TrackerPayload payload) { - boolean result = eventStore.add(payload); + boolean result = eventStore.addEvent(payload); + + if (!isClosing) { + if (eventStore.size() >= batchSize) { + executor.schedule(getPostRequestRunnable(batchSize), retryDelay.get(), TimeUnit.MILLISECONDS); + } + } if (!result) { LOGGER.error("Unable to add payload to emitter, emitter buffer is full"); } } - /* - * Forces all the payloads currently in the buffer to be sent + /** + * Forces all the payloads currently in the buffer to be sent immediately */ @Override public void flushBuffer() { - drainEventsAndSend(eventStore.getSize()); + executor.schedule(getPostRequestRunnable(eventStore.size()), 0, TimeUnit.MILLISECONDS); } /** @@ -147,49 +164,52 @@ public void setBatchSize(final int batchSize) { */ @Override public int getBatchSize() { - return this.batchSize; - } - - /** - * Checks if batchSize is reached - * - * @return the new Runnable object - */ - private Runnable getCheckForEventsToSendRunnable() { - return () -> { - while (!isClosing) { - if (eventStore.getSize() >= batchSize) { - drainEventsAndSend(this.getBatchSize()); - } - } - }; + return batchSize; } - private void drainEventsAndSend(int numberOfEvents) { - List payloads = eventStore.removeEvents(numberOfEvents); - execute(getPostRequestRunnable(payloads)); + long getRetryDelay() { + return retryDelay.get(); } /** * Returns a Runnable POST Request operation * - * @param buffer the event buffer to be sent + * @param numberOfEvents the number of events to be sent in the request * @return the new Runnable object */ - private Runnable getPostRequestRunnable(final List buffer) { + private Runnable getPostRequestRunnable(int numberOfEvents) { return () -> { - if (buffer.size() == 0) { - return; - } + BatchPayload batchedEvents = null; + try { + batchedEvents = eventStore.getEventsBatch(numberOfEvents); + List eventsInRequest = batchedEvents.getPayloads(); - final SelfDescribingJson post = getFinalPost(buffer); - final int code = httpClientAdapter.post(post); + if (eventsInRequest.size() == 0) { + return; + } - // Process results - if (!isSuccessfulSend(code)) { - LOGGER.error("BatchEmitter failed to send {} events: code: {}", buffer.size(), code); - } else { - LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", buffer.size(), code); + final SelfDescribingJson post = getFinalPost(eventsInRequest); + final int code = httpClientAdapter.post(post); + + // Process results + if (isSuccessfulSend(code)) { + LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", eventsInRequest.size(), code); + retryDelay.set(0L); + eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); + } else { + LOGGER.error("BatchEmitter failed to send {} events: code: {}", eventsInRequest.size(), code); + eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + + // exponentially increase retry backoff time after the first failure + if (!retryDelay.compareAndSet(0, 50L)) { + retryDelay.updateAndGet(currentDelay -> currentDelay * 2); + } + } + } catch (Exception e) { + LOGGER.error("BatchEmitter event sending error: {}", e.getMessage()); + if (batchedEvents != null) { + eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + } } }; } @@ -197,14 +217,14 @@ private Runnable getPostRequestRunnable(final List buffer) { /** * Constructs the SelfDescribingJson to be sent to the endpoint * - * @param buffer the event buffer + * @param events the event buffer * @return the constructed POST payload */ - private SelfDescribingJson getFinalPost(final List buffer) { + private SelfDescribingJson getFinalPost(final List events) { final List> toSendPayloads = new ArrayList<>(); final String sentTimestamp = Long.toString(System.currentTimeMillis()); - for (TrackerPayload payload : buffer) { + for (TrackerPayload payload : events) { payload.add(Parameter.DEVICE_SENT_TIMESTAMP, sentTimestamp); toSendPayloads.add(payload.getMap()); } @@ -213,13 +233,16 @@ private SelfDescribingJson getFinalPost(final List buffer) { } /** - * On close attempt to send all remaining events. + * On close, attempt to send all remaining events. + *

+ * Implementation note: Be aware that calling `close()` + * has a side-effect of shutting down the Emitter ScheduledExecutorService. */ @Override public void close() { + final long closeTimeout = 5; isClosing = true; - checkForEventsToSend.interrupt(); // Kill checkForEventsToSend thread flushBuffer(); // Attempt to send all remaining events //Shutdown executor threadpool diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java new file mode 100644 index 00000000..f561e1aa --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; + +import java.util.List; + + +public class BatchPayload { + + private final Long batchId; + private final List payloads; + + public BatchPayload(Long payloadId, List payloads) { + this.batchId = payloadId; + this.payloads = payloads; + } + + public Long getBatchId() { + return batchId; + } + + public List getPayloads() { + return payloads; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index 4f61e1d3..9a2eaa6a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -6,11 +6,13 @@ public interface EventStore { - boolean add(TrackerPayload trackerPayload); + boolean addEvent(TrackerPayload trackerPayload); - List removeEvents(int numberToRemove); - - int getSize(); + BatchPayload getEventsBatch(int numberToRemove); List getAllEvents(); + + void cleanupAfterSendingAttempt(boolean successfullySent, long batchId); + + int size(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index e3ab9477..d5366da5 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -1,36 +1,75 @@ package com.snowplowanalytics.snowplow.tracker.emitter; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicLong; public class InMemoryEventStore implements EventStore { - public final BlockingQueue eventBuffer = new LinkedBlockingQueue<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryEventStore.class); + private final AtomicLong batchId = new AtomicLong(1); + + public final LinkedBlockingDeque eventBuffer; + public final ConcurrentHashMap> eventsBeingSent = new ConcurrentHashMap<>(); + + public InMemoryEventStore() { + eventBuffer = new LinkedBlockingDeque<>(); + } + + public InMemoryEventStore(int bufferCapacity) { + eventBuffer = new LinkedBlockingDeque<>(bufferCapacity); + } @Override - public boolean add(TrackerPayload trackerPayload) { + public boolean addEvent(TrackerPayload trackerPayload) { return eventBuffer.offer(trackerPayload); } @Override - public List removeEvents(int numberToRemove) { - // if numberToRemove is greater than the number of events present, - // it will return all the events (there's no error) - List eventsList = new ArrayList<>(); - eventBuffer.drainTo(eventsList, numberToRemove); - return eventsList; + public BatchPayload getEventsBatch(int numberToGet) { + List eventsToSend = new ArrayList<>(); + + eventBuffer.drainTo(eventsToSend, numberToGet); + + // The batch of events is wrapped as a BatchPayload + // They're also added to the "pending" event buffer, the eventsBeingSent HashMap + BatchPayload batchedEvents = new BatchPayload(batchId.getAndIncrement(), eventsToSend); + eventsBeingSent.put(batchedEvents.getBatchId(), batchedEvents.getPayloads()); + return batchedEvents; } @Override - public int getSize() { - return eventBuffer.size(); + public void cleanupAfterSendingAttempt(boolean successfullySent, long batchId) { + // Events that successfully sent are deleted from the pending buffer + List events = eventsBeingSent.remove(batchId); + + // Events that didn't send are inserted at the head of the eventBuffer + // for immediate resending. + if (!successfullySent) { + while (events.size() > 0) { + TrackerPayload payloadToReinsert = events.remove(0); + boolean result = eventBuffer.offerFirst(payloadToReinsert); + if (!result) { + LOGGER.error("Event buffer is full. Dropping newer payload to reinsert older payload"); + eventBuffer.removeLast(); + eventBuffer.offerFirst(payloadToReinsert); + } + } + } } @Override public List getAllEvents() { - return new ArrayList<>(eventBuffer); + TrackerPayload[] events = eventBuffer.toArray(new TrackerPayload[0]); + return Arrays.asList(events); + } + + @Override + public int size() { + return eventBuffer.size(); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 14d49327..715da53b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -50,9 +50,14 @@ protected SimpleEmitter(final Builder builder) { super(builder); } + /** + * Adds an event to the buffer and instantly sends it + * + * @param payload a payload + */ @Override public void add(TrackerPayload payload) { - // nothing happens + executor.execute(getGetRequestRunnable(payload)); } /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 31161c0e..92bb8988 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -214,7 +214,7 @@ public Subject getSubject() { /** * Adds the default parameters to a TrackerPayload object. * - * @param payload the payload to add too. + * @param payload the payload to add to. * @return the TrackerPayload with appended values. */ protected TrackerPayload putDefaultParams(TrackerPayload payload) { diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 533c0385..b2840266 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -15,6 +15,7 @@ import java.util.*; import static java.util.Collections.singletonList; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchPayload; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -524,7 +525,7 @@ public void testTrackTimingWithSubject() throws InterruptedException { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); - assertEquals("java-0.11.0", tracker.getTrackerVersion()); + assertEquals("java-0.12.0-alpha.1", tracker.getTrackerVersion()); } @Test @@ -570,31 +571,4 @@ public void testSetNamespace() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); assertEquals("namespace", tracker.getNamespace()); } - - @Test - public void threadsHaveExpectedNames() { - // A new thread should be created for each event tracked, - // up to the configurable pool size limit - tracker.track(PageView.builder() - .pageUrl("url") - .pageTitle("title") - .referrer("referer") - .build()); - - tracker.track(PageView.builder() - .pageUrl("url") - .pageTitle("title") - .referrer("referer") - .build()); - - // Create a list of all live thread names - List threadList = new ArrayList<>(Thread.getAllStackTraces().keySet()); - List threadNames = new ArrayList<>(); - for (Thread thread : threadList) { - threadNames.add(thread.getName()); - } - - Assert.assertTrue(threadNames.contains("snowplow-tracker-pool-1-event-thread-1")); - Assert.assertTrue(threadNames.contains("snowplow-tracker-pool-1-event-thread-2")); - } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 05c570c6..5c310e41 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; import com.google.common.collect.Lists; @@ -35,16 +36,19 @@ public class BatchEmitterTest { private MockHttpClientAdapter mockHttpClientAdapter; + private FailingHttpClientAdapter failingHttpClientAdapter; private BatchEmitter emitter; public static class MockHttpClientAdapter implements HttpClientAdapter { public boolean isGetCalled = false; public boolean isPostCalled = false; + public int postCounter = 0; public SelfDescribingJson capturedPayload; @Override public int post(SelfDescribingJson payload) { isPostCalled = true; + postCounter++; capturedPayload = payload; return 200; } @@ -66,9 +70,42 @@ public Object getHttpClient() { } } + // this class fails to "send" the first 4 requests + // but returns a successful result (200) subsequently + static class FailingHttpClientAdapter implements HttpClientAdapter { + int failedPostCounter = 0; + int successfulPostCounter = 0; + @Override + public int post(SelfDescribingJson payload) { + if (failedPostCounter >= 4) { + successfulPostCounter++; + return 200; + } + + failedPostCounter++; + return 500; + } + + @Override + public int get(TrackerPayload payload) { + return 0; + } + + @Override + public String getUrl() { + return null; + } + + @Override + public Object getHttpClient() { + return null; + } + } + @Before public void setUp() { mockHttpClientAdapter = new MockHttpClientAdapter(); + failingHttpClientAdapter = new FailingHttpClientAdapter(); emitter = BatchEmitter.builder() .httpClientAdapter(mockHttpClientAdapter) .batchSize(10) @@ -102,8 +139,23 @@ public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Interrupte @SuppressWarnings("unchecked") List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); - assertPayload(payloads, capturedPayload); Assert.assertEquals(0, emitter.getBuffer().size()); + Assert.assertEquals(1, mockHttpClientAdapter.postCounter); + } + + @Test + public void addToBuffer_doesNotAddEventIfBufferFull() { + emitter = BatchEmitter.builder() + .httpClientAdapter(mockHttpClientAdapter) + .bufferCapacity(1) + .build(); + + emitter.add(createPayload()); + + TrackerPayload differentPayload = createPayload(); + emitter.add(differentPayload); + + Assert.assertFalse(emitter.getBuffer().contains(differentPayload)); } @Test @@ -164,25 +216,9 @@ public void getFinalPost_shouldAddSTMParameter() throws InterruptedException { } } - @Test - public void emitterThreadFactory_correctlyNamesThreads() { - class MyRunnable implements Runnable { - @Override - public void run() {} - } - - BatchEmitter.EmitterThreadFactory threadFactory = new BatchEmitter.EmitterThreadFactory(); - String threadName = threadFactory.newThread(new MyRunnable()).getName(); - - // It's pool-2 because pool-1 was created during emitter instantiation - Assert.assertEquals("snowplow-emitter-pool-2-request-thread-1", threadName); - } - @Test public void threadsHaveExpectedNames() { - // A checkForEventsToSend thread is created on BatchEmitter instantiation. - // Calling flushBuffer() here to require another thread - causing - // creation of a request thread within the scheduledThreadPool. + // Calling flushBuffer() here to create a request thread for event sending emitter.flushBuffer(); // Create a list of all live thread names @@ -192,8 +228,16 @@ public void threadsHaveExpectedNames() { threadNames.add(thread.getName()); } - Assert.assertTrue(threadNames.contains("snowplow-emitter-checkForEvents-thread-1")); - Assert.assertTrue(threadNames.contains("snowplow-emitter-pool-1-request-thread-1")); + // Because the threadpools are named by a static ThreadFactory, + // the pool number varies if this test is run in isolation or not + boolean matchResult = false; + for (String name : threadNames) { + if (Pattern.matches("snowplow-emitter-pool-\\d+-request-thread-1", name)) { + matchResult = true; + } + } + + Assert.assertTrue(matchResult); } @Test @@ -202,6 +246,8 @@ public void close_sendsEventsAndStopsThreads() throws InterruptedException { for (TrackerPayload payload : payloads) { emitter.add(payload); } + Thread.sleep(500); + emitter.close(); Thread.sleep(500); @@ -218,6 +264,65 @@ public void close_sendsEventsAndStopsThreads() throws InterruptedException { Assert.assertEquals(20, emitter.getBuffer().size()); } + @Test + public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedException { + emitter = BatchEmitter.builder() + .httpClientAdapter(new FailingHttpClientAdapter()) + .batchSize(10) + .build(); + + List payloads = createPayloads(2); + for (TrackerPayload payload : payloads) { + emitter.add(payload); + } + emitter.flushBuffer(); + Thread.sleep(500); + + List storedEvents = emitter.getBuffer(); + + Assert.assertEquals(2, storedEvents.size()); + Assert.assertTrue(storedEvents.contains(payloads.get(0))); + Assert.assertTrue(storedEvents.contains(payloads.get(1))); + } + + @Test + public void eventSendingFailureIncreasesBackoffTime() throws InterruptedException { + emitter = BatchEmitter.builder() + .httpClientAdapter(failingHttpClientAdapter) + .batchSize(1) + .build(); + + List payloads = createPayloads(2); + for (TrackerPayload payload : payloads) { + emitter.add(payload); + } + Thread.sleep(500); + + Assert.assertEquals(100, emitter.getRetryDelay()); + } + + @Test + public void successfulSendAfterFailureResetsBackoffTime() throws InterruptedException { + // the FailingHttpClientAdapter returns 500 for the first 4 requests + // then subsequently returns 200 + FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); + emitter = BatchEmitter.builder() + .httpClientAdapter(failingHttpClientAdapter) + .batchSize(1) + .threadCount(1) + .build(); + + List payloads = createPayloads(6); + for (TrackerPayload payload : payloads) { + emitter.add(payload); + } + + Thread.sleep(500); + + Assert.assertEquals(2, failingHttpClientAdapter.successfulPostCounter); + Assert.assertEquals(0, emitter.getRetryDelay()); + } + private TrackerPayload createPayload() { PageView pv = PageView.builder() .pageUrl("https://www.snowplowanalytics.com/") diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java index 7c341dfb..33a7d843 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -18,71 +18,101 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; import java.util.List; public class InMemoryEventStoreTest { private TrackerPayload trackerPayload; private InMemoryEventStore eventStore; - private List singleEventList; - private List twoEventsList; - @Before public void setUp() { - trackerPayload = createPayload(); + trackerPayload = createTrackerPayload(); eventStore = new InMemoryEventStore(); - singleEventList = new ArrayList<>(); - twoEventsList = new ArrayList<>(); - - singleEventList.add(trackerPayload); - twoEventsList.add(trackerPayload); - twoEventsList.add(trackerPayload); } @Test public void correctlyAddAnEventToStore() { - boolean result = eventStore.add(trackerPayload); + boolean result = eventStore.addEvent(trackerPayload); Assert.assertTrue(result); } @Test public void getSize_returnsCorrectNumberOfStoredEvents() { - storeTwoPayloads(); + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + + Assert.assertEquals(2, eventStore.size()); + } + + @Test + public void getEventsFromStorage() { + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + + Assert.assertEquals(2, eventStore.getEventsBatch(2).getPayloads().size()); + Assert.assertEquals(2, eventStore.size()); + } + + @Test + public void getAllEventsIfAskedForMoreEventsThanAreStored() { + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + + List events = eventStore.getEventsBatch(3).getPayloads(); - Assert.assertEquals(2, eventStore.getSize()); + Assert.assertEquals(2, events.size()); } @Test - public void removeAddedEvent() { - storeTwoPayloads(); + public void putEventsBackInBufferIfFailedToSend() { + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + eventStore.getEventsBatch(2); - List removedEventList = eventStore.removeEvents(1); - Assert.assertEquals(singleEventList, removedEventList); - Assert.assertEquals(1, eventStore.getSize()); + Assert.assertEquals(0, eventStore.size()); + + eventStore.cleanupAfterSendingAttempt(false, 1L); + + Assert.assertEquals(2, eventStore.size()); } @Test - public void removeAllEventsIfAskedForMoreEventsThanAreStored() { - storeTwoPayloads(); + public void doNotPutEventsBackInBufferIfSent() { + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + eventStore.getEventsBatch(2); + + Assert.assertEquals(0, eventStore.size()); - List removedEventList = eventStore.removeEvents(100); - Assert.assertEquals(twoEventsList, removedEventList); - Assert.assertEquals(0, eventStore.getSize()); + eventStore.cleanupAfterSendingAttempt(true, 1L); + + Assert.assertEquals(0, eventStore.size()); } @Test - public void getAllEvents_doesNotRemoveEventsFromStore() { - storeTwoPayloads(); + public void dropNewerEventsOnFailureWhenBufferFull() { + eventStore = new InMemoryEventStore(3); + + TrackerPayload differentPayload = createTrackerPayload(); + + eventStore.addEvent(differentPayload); + eventStore.getEventsBatch(1); + + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + eventStore.addEvent(trackerPayload); + + eventStore.cleanupAfterSendingAttempt(false, 1L); + Assert.assertEquals(3, eventStore.size()); + Assert.assertTrue(eventStore.getAllEvents().contains(differentPayload)); - List retrievedEventsList = eventStore.getAllEvents(); - Assert.assertEquals(twoEventsList, retrievedEventsList); - Assert.assertEquals(2, eventStore.getSize()); } - private TrackerPayload createPayload() { + private TrackerPayload createTrackerPayload() { PageView pv = PageView.builder() .pageUrl("https://www.snowplowanalytics.com/") .pageTitle("Snowplow") @@ -91,10 +121,4 @@ private TrackerPayload createPayload() { return pv.getPayload(); } - - private void storeTwoPayloads() { - for (TrackerPayload payload : twoEventsList) { - eventStore.add(payload); - } - } } From 38aba2410447faee08b9f0f71a2657338a6bd979 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Tue, 8 Mar 2022 11:15:53 +0000 Subject: [PATCH 077/128] Return eventId from Tracker.track() (close #304) * Remove deviceCreatedTimestamp and eventId from AbstractEvent builder * Set eid and dtm at TrackerPayload creation * Remove method calls from Tracker (tests failing) * Add tests to TrackerPayload * Remove Hamcrest dependency * Return eventId from Tracker.track() * Add eid and dtm to TrackerPayload map directly * Improve clarity of Tracker tests --- build.gradle | 1 - .../snowplow/tracker/Subject.java | 2 + .../snowplow/tracker/Tracker.java | 54 ++-- .../tracker/emitter/AbstractEmitter.java | 2 +- .../tracker/emitter/BatchEmitter.java | 7 +- .../snowplow/tracker/emitter/Emitter.java | 2 +- .../tracker/emitter/SimpleEmitter.java | 6 +- .../tracker/events/AbstractEvent.java | 77 +----- .../tracker/events/EcommerceTransaction.java | 2 +- .../events/EcommerceTransactionItem.java | 18 +- .../snowplow/tracker/events/Event.java | 17 -- .../snowplow/tracker/events/PageView.java | 2 +- .../snowplow/tracker/events/ScreenView.java | 8 +- .../snowplow/tracker/events/Structured.java | 2 +- .../snowplow/tracker/events/Unstructured.java | 2 +- .../tracker/payload/TrackerPayload.java | 20 ++ .../snowplow/tracker/TrackerTest.java | 247 ++++++++++-------- .../tracker/emitter/BatchEmitterTest.java | 43 +-- .../tracker/http/HttpClientAdapterTest.java | 7 +- .../payload/SelfDescribingJsonTest.java | 6 +- .../tracker/payload/TrackerPayloadTest.java | 28 ++ 21 files changed, 266 insertions(+), 287 deletions(-) diff --git a/build.gradle b/build.gradle index 639091a3..8fea99c3 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,6 @@ dependencies { testCompileOnly 'junit:junit:4.13' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' - testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index c61958e8..1b0ff8f3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -21,6 +21,8 @@ /** * An object for managing extra event decoration. + * All the properties are optional. However, the timezone is set by default, + * to that of the server. */ public class Subject { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 8079ab9b..abf41b58 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -187,7 +187,7 @@ public DevicePlatform getPlatform() { * @return the wrapper containing the Tracker parameters */ public TrackerParameters getParameters() { - return this.parameters; + return parameters; } // --- Event Tracking Functions @@ -195,21 +195,41 @@ public TrackerParameters getParameters() { /** * Handles tracking the different types of events that * the Tracker can encounter. + * A TrackerPayload object - or more than one, in the case of eCommerceTransaction events - + * will be created from the Event. This is passed to the configured Emitter. + * If the event was successfully added to the Emitter buffer for sending, + * a list containing the payload's eventId string (a UUID) is returned. + * EcommerceTransactions will return all the relevant eventIds in the list. + * If the Emitter event buffer is full, the payload will be lost. In this case, this method + * returns a list containing null. + *

+ * Implementation note: As a side effect of adding a payload to the Emitter, + * it triggers an Emitter thread to emit a batch of events. * * @param event the event to track + * @return a list of eventIDs (UUIDs) */ - public void track(Event event) { + public List track(Event event) { + List results = new ArrayList<>(); // a list because Ecommerce events become multiple Payloads List processedEvents = eventTypeSpecificPreProcessing(event); for (Event processedEvent : processedEvents) { - // Event ID (eid) and device_created_timestamp (dtm) are generated when the Event is initialised + // Event ID (eid) and device_created_timestamp (dtm) are generated when + // the TrackerPayload is created TrackerPayload payload = (TrackerPayload) processedEvent.getPayload(); addTrackerParameters(payload); addContext(processedEvent, payload); addSubject(processedEvent, payload); - this.emitter.add(payload); + + boolean addedToBuffer = emitter.add(payload); + if (addedToBuffer) { + results.add(payload.getEventId()); + } else { + results.add(null); + } } + return results; } private List eventTypeSpecificPreProcessing(Event event) { @@ -223,7 +243,7 @@ private List eventTypeSpecificPreProcessing(Event event) { if (eventClass.equals(Unstructured.class)) { // Need to set the Base64 rule for Unstructured events final Unstructured unstructured = (Unstructured) event; - unstructured.setBase64Encode(this.parameters.getBase64Encoded()); + unstructured.setBase64Encode(parameters.getBase64Encoded()); eventList.add(unstructured); } else if (eventClass.equals(EcommerceTransaction.class)) { @@ -231,23 +251,19 @@ private List eventTypeSpecificPreProcessing(Event event) { eventList.add(ecommerceTransaction); // Track each item individually - for (final EcommerceTransactionItem item : ecommerceTransaction.getItems()) { - item.setDeviceCreatedTimestamp(ecommerceTransaction.getDeviceCreatedTimestamp()); - eventList.add(item); - } + eventList.addAll(ecommerceTransaction.getItems()); + } else if (eventClass.equals(Timing.class) || eventClass.equals(ScreenView.class)) { // Timing and ScreenView events are wrapper classes for Unstructured events // Need to create Unstructured events from them to send. final Unstructured unstructured = Unstructured.builder() .eventData((SelfDescribingJson) event.getPayload()) .customContext(event.getContext()) - .deviceCreatedTimestamp(event.getDeviceCreatedTimestamp()) .trueTimestamp(event.getTrueTimestamp()) - .eventId(event.getEventId()) .subject(event.getSubject()) .build(); - unstructured.setBase64Encode(this.parameters.getBase64Encoded()); + unstructured.setBase64Encode(parameters.getBase64Encoded()); eventList.add(unstructured); } else { @@ -257,10 +273,10 @@ private List eventTypeSpecificPreProcessing(Event event) { } private void addTrackerParameters(TrackerPayload payload) { - payload.add(Parameter.PLATFORM, this.parameters.getPlatform().toString()); - payload.add(Parameter.APP_ID, this.parameters.getAppId()); - payload.add(Parameter.NAMESPACE, this.parameters.getNamespace()); - payload.add(Parameter.TRACKER_VERSION, this.parameters.getTrackerVersion()); + payload.add(Parameter.PLATFORM, parameters.getPlatform().toString()); + payload.add(Parameter.APP_ID, parameters.getAppId()); + payload.add(Parameter.NAMESPACE, parameters.getNamespace()); + payload.add(Parameter.TRACKER_VERSION, parameters.getTrackerVersion()); } private void addContext(Event event, TrackerPayload payload) { @@ -269,7 +285,7 @@ private void addContext(Event event, TrackerPayload payload) { // Build the final context and add it to the payload if (entities != null && entities.size() > 0) { SelfDescribingJson envelope = getFinalContext(entities); - payload.addMap(envelope.getMap(), this.parameters.getBase64Encoded(), Parameter.CONTEXT_ENCODED, Parameter.CONTEXT); + payload.addMap(envelope.getMap(), parameters.getBase64Encoded(), Parameter.CONTEXT_ENCODED, Parameter.CONTEXT); } } @@ -293,8 +309,8 @@ private void addSubject(Event event, TrackerPayload payload) { // Add subject if available if (eventSubject != null) { payload.addMap(new HashMap<>(eventSubject.getSubject())); - } else if (this.subject != null) { - payload.addMap(new HashMap<>(this.subject.getSubject())); + } else if (subject != null) { + payload.addMap(new HashMap<>(subject.getSubject())); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index e56e4c14..00779196 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -133,7 +133,7 @@ protected AbstractEmitter(final Builder builder) { * @param payload an payload */ @Override - public abstract void add(TrackerPayload payload); + public abstract boolean add(TrackerPayload payload); /** * Customize the emitter batch size to any valid integer greater than zero. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index c76951eb..c13ea4fe 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -108,13 +108,12 @@ protected BatchEmitter(final Builder builder) { /** * Adds a TrackerPayload to the concurrent queue buffer *

- * Implementation note: Be aware that calling `close()` on a BatchEmitter instance - * has a side-effect and will shutdown that ExecutorService. + * Implementation note: As a side effect it triggers an Emitter thread to emit a batch of events. * * @param payload a payload */ @Override - public void add(final TrackerPayload payload) { + public boolean add(final TrackerPayload payload) { boolean result = eventStore.addEvent(payload); if (!isClosing) { @@ -126,6 +125,8 @@ public void add(final TrackerPayload payload) { if (!result) { LOGGER.error("Unable to add payload to emitter, emitter buffer is full"); } + + return result; } /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index aac70315..c9cb9b50 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -27,7 +27,7 @@ public interface Emitter { * * @param payload a payload to be emitted */ - void add(TrackerPayload payload); + boolean add(TrackerPayload payload); /** * Customize the emitter batch size to any valid integer diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 715da53b..a6cd3bf8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -56,8 +56,12 @@ protected SimpleEmitter(final Builder builder) { * @param payload a payload */ @Override - public void add(TrackerPayload payload) { + public boolean add(TrackerPayload payload) { executor.execute(getGetRequestRunnable(payload)); + + // This result doesn't mean anything + // The return type is for BatchEmitter's benefit + return true; } /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 92bb8988..369bb3a1 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -40,22 +40,16 @@ public abstract class AbstractEvent implements Event { protected final List context; - protected long deviceCreatedTimestamp; - /** * The true timestamp may be null if none is set. */ protected Long trueTimestamp; - - protected final String eventId; protected final Subject subject; public static abstract class Builder> { private List context = new LinkedList<>(); - private long deviceCreatedTimestamp = System.currentTimeMillis(); protected Long trueTimestamp = null; - private String eventId = Utils.getEventId(); private Subject subject = null; protected abstract T self(); @@ -71,31 +65,6 @@ public T customContext(List context) { return self(); } - /** - * A custom event timestamp. - * - * @param timestamp the event timestamp as - * unix epoch - * @return itself - * Use {@link #trueTimestamp} or {@link #deviceCreatedTimestamp} - */ - @Deprecated - public T timestamp(long timestamp) { - return deviceCreatedTimestamp(timestamp); - } - - /** - * Adjust the device-created timestamp. This is usually not what you want, check {@link #trueTimestamp}. - * - * @param timestamp the event timestamp as - * unix epoch - * @return itself - */ - public T deviceCreatedTimestamp(long timestamp) { - this.deviceCreatedTimestamp = timestamp; - return self(); - } - /** * The true timestamp of that event (as determined by the user). * @@ -108,17 +77,6 @@ public T trueTimestamp(Long timestamp) { return self(); } - /** - * A custom eventId for the event. - * - * @param eventId the eventId - * @return itself - */ - public T eventId(String eventId) { - this.eventId = eventId; - return self(); - } - /** * A custom subject for the event. * @@ -146,13 +104,9 @@ protected AbstractEvent(Builder builder) { // Precondition checks Preconditions.checkNotNull(builder.context); - Preconditions.checkNotNull(builder.eventId); - Preconditions.checkArgument(!builder.eventId.isEmpty(), "eventId cannot be empty"); this.context = builder.context; - this.deviceCreatedTimestamp = builder.deviceCreatedTimestamp; this.trueTimestamp = builder.trueTimestamp; - this.eventId = builder.eventId; this.subject = builder.subject; } @@ -164,23 +118,6 @@ public List getContext() { return new ArrayList<>(this.context); } - /** - * @return the event's timestamp - * @deprecated Use {@link #getTrueTimestamp()} or {@link #getDeviceCreatedTimestamp()} - */ - @Override - public long getTimestamp() { - return this.deviceCreatedTimestamp; - } - - /** - * @return the event's device created timestamp. - */ - @Override - public long getDeviceCreatedTimestamp() { - return deviceCreatedTimestamp; - } - /** * @return the event's true timestamp. */ @@ -189,14 +126,6 @@ public Long getTrueTimestamp() { return trueTimestamp; } - /** - * @return the event id - */ - @Override - public String getEventId() { - return this.eventId; - } - /** * @return the event subject */ @@ -217,12 +146,10 @@ public Subject getSubject() { * @param payload the payload to add to. * @return the TrackerPayload with appended values. */ - protected TrackerPayload putDefaultParams(TrackerPayload payload) { - payload.add(Parameter.EID, getEventId()); - if (getTrueTimestamp()!=null) { + protected TrackerPayload putTrueTimestamp(TrackerPayload payload) { + if (getTrueTimestamp() != null) { payload.add(Parameter.TRUE_TIMESTAMP, Long.toString(getTrueTimestamp())); } - payload.add(Parameter.DEVICE_CREATED_TIMESTAMP, Long.toString(getDeviceCreatedTimestamp())); return payload; } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index d7f31752..9151e014 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -209,7 +209,7 @@ public TrackerPayload getPayload() { payload.add(Parameter.TR_STATE, this.state); payload.add(Parameter.TR_COUNTRY, this.country); payload.add(Parameter.TR_CURRENCY, this.currency); - return putDefaultParams(payload); + return putTrueTimestamp(payload); } /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index 0a928f4c..37a89cbc 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -140,15 +140,6 @@ protected EcommerceTransactionItem(Builder builder) { this.currency = builder.currency; } - /** - * @param timestamp the new timestamp - * Use {@link #setTrueTimestamp(long)} or {@link #setTrueTimestamp(long)} - */ - @Deprecated - public void setTimestamp(long timestamp) { - setDeviceCreatedTimestamp(timestamp); - } - /** * @param timestamp the new timestamp */ @@ -156,13 +147,6 @@ public void setTrueTimestamp(long timestamp) { this.trueTimestamp = timestamp; } - /** - * @param timestamp the new timestamp - */ - public void setDeviceCreatedTimestamp(Long timestamp) { - this.deviceCreatedTimestamp = timestamp; - } - /** * Returns a TrackerPayload which can be stored into * the local database. @@ -179,6 +163,6 @@ public TrackerPayload getPayload() { payload.add(Parameter.TI_ITEM_PRICE, Double.toString(this.price)); payload.add(Parameter.TI_ITEM_QUANTITY, Integer.toString(this.quantity)); payload.add(Parameter.TI_ITEM_CURRENCY, this.currency); - return putDefaultParams(payload); + return putTrueTimestamp(payload); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 935279a1..b9909913 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -28,28 +28,11 @@ public interface Event { */ List getContext(); - /** - * @return the event's timestamp - * Use {@link #getTrueTimestamp()} or {@link #getDeviceCreatedTimestamp()} - */ - @Deprecated - long getTimestamp(); - /** * @return the event's true timestamp */ Long getTrueTimestamp(); - /** - * @return the event's device created timestamp - */ - long getDeviceCreatedTimestamp(); - - /** - * @return the event id - */ - String getEventId(); - /** * @return the event subject */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index 27bed72f..87da01f4 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -102,6 +102,6 @@ public TrackerPayload getPayload() { payload.add(Parameter.PAGE_URL, this.pageUrl); payload.add(Parameter.PAGE_TITLE, this.pageTitle); payload.add(Parameter.PAGE_REFR, this.referrer); - return putDefaultParams(payload); + return putTrueTimestamp(payload); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index d84adc57..9b1bf1a0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -19,6 +19,8 @@ import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import java.util.LinkedHashMap; + public class ScreenView extends AbstractEvent { private final String name; @@ -79,9 +81,9 @@ protected ScreenView(Builder builder) { * @return the payload as a SelfDescribingJson. */ public SelfDescribingJson getPayload() { - TrackerPayload payload = new TrackerPayload(); - payload.add(Parameter.SV_ID, this.id); - payload.add(Parameter.SV_NAME, this.name); + LinkedHashMap payload = new LinkedHashMap<>(); + payload.put(Parameter.SV_ID, this.id); + payload.put(Parameter.SV_NAME, this.name); return new SelfDescribingJson(Constants.SCHEMA_SCREEN_VIEW, payload); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index e80da843..06e7f282 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -131,6 +131,6 @@ public TrackerPayload getPayload() { payload.add(Parameter.SE_PROPERTY, this.property); payload.add(Parameter.SE_VALUE, this.value != null ? Double.toString(this.value) : null); - return putDefaultParams(payload); + return putTrueTimestamp(payload); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index a73b4575..37174422 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -89,6 +89,6 @@ public TrackerPayload getPayload() { payload.add(Parameter.EVENT, Constants.EVENT_UNSTRUCTURED); payload.addMap(envelope.getMap(), this.base64Encode, Parameter.UNSTRUCTURED_ENCODED, Parameter.UNSTRUCTURED); - return putDefaultParams(payload); + return putTrueTimestamp(payload); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 93bb9303..21af7f98 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -16,6 +16,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,25 @@ public class TrackerPayload implements Payload { private static final Logger LOGGER = LoggerFactory.getLogger(TrackerPayload.class); protected final Map payload = new LinkedHashMap<>(); + private final String eventId; + private final Long deviceCreatedTimestamp; + + + public TrackerPayload() { + eventId = Utils.getEventId(); + deviceCreatedTimestamp = System.currentTimeMillis(); + + add(Parameter.EID, eventId); + add(Parameter.DEVICE_CREATED_TIMESTAMP, Long.toString(deviceCreatedTimestamp)); + } + + public String getEventId() { + return eventId; + } + + public Long getDeviceCreatedTimestamp() { + return deviceCreatedTimestamp; + } /** * Add a key-value pair to the payload: - Checks that the key is not null or diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index b2840266..3cb441da 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -15,8 +15,6 @@ import java.util.*; import static java.util.Collections.singletonList; -import com.snowplowanalytics.snowplow.tracker.emitter.BatchPayload; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; @@ -31,31 +29,23 @@ public class TrackerTest { public static final String EXPECTED_CONTEXTS = "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1\",\"data\":[{\"schema\":\"schema\",\"data\":{\"foo\":\"bar\"}}]}"; - public static final String EXPECTED_EVENT_ID = "15e9b149-6029-4f6e-8447-5b9797c9e6be"; public static class MockEmitter implements Emitter { public ArrayList eventList = new ArrayList<>(); @Override - public void add(TrackerPayload payload) { + public boolean add(TrackerPayload payload) { eventList.add(payload); + return true; } - @Override public void setBatchSize(int batchSize) {} - @Override public void flushBuffer() {} - @Override - public int getBatchSize() { - return 0; - } - + public int getBatchSize() { return 0; } @Override - public List getBuffer() { - return null; - } + public List getBuffer() { return null; } } MockEmitter mockEmitter; @@ -75,6 +65,57 @@ public void setUp() { // --- Event Tests + @Test + public void testTrackReturnsEventIdIfSuccessful() throws InterruptedException { + // a list to allow for eCommerceTransaction + List result = tracker.track(Unstructured.builder() + .eventData(new SelfDescribingJson( + "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", + ImmutableMap.of("foo", "bar") + )) + .build()); + + Thread.sleep(500); + + boolean isValidEventId = true; + try { + UUID.fromString(result.get(0)); + } catch (Exception e) { + isValidEventId = false; + } + + assertTrue(isValidEventId); + } + + @Test + public void testTrackReturnsNullIfEventWasDropped() throws InterruptedException { + class FailingMockEmitter implements Emitter { + @Override + public boolean add(TrackerPayload payload) { return false; } + @Override + public void setBatchSize(int batchSize) {} + @Override + public void flushBuffer() {} + @Override + public int getBatchSize() { return 0; } + @Override + public List getBuffer() { return null; } + } + FailingMockEmitter failingMockEmitter = new FailingMockEmitter(); + tracker = new Tracker.TrackerBuilder(failingMockEmitter, "AF003", "cloudfront").build(); + + List result = tracker.track(Unstructured.builder() + .eventData(new SelfDescribingJson( + "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", + ImmutableMap.of("foo", "bar") + )) + .build()); + + Thread.sleep(500); + + assertNull(result.get(0)); + } + @Test public void testEcommerceEvent() throws InterruptedException { // Given @@ -87,9 +128,7 @@ public void testEcommerceEvent() throws InterruptedException { .category("category") .currency("currency") .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build(); // When @@ -105,9 +144,7 @@ public void testEcommerceEvent() throws InterruptedException { .currency("currency") .items(item) .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then @@ -117,15 +154,13 @@ public void testEcommerceEvent() throws InterruptedException { assertEquals(2, results.size()); Map result1 = results.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected1 = ImmutableMap.builder() .put("e", "tr") .put("tr_cu", "currency") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("aid", "cloudfront") .put("tr_sh", "3.0") - .put("dtm", "123456") .put("ttm", "456789") .put("tz", "Etc/UTC") .put("tr_co", "country") @@ -137,19 +172,19 @@ public void testEcommerceEvent() throws InterruptedException { .put("tr_tt", "1.0") .put("tr_ci", "city") .put("tr_st", "state") - .build(), result1); + .build(); + + assertTrue(result1.entrySet().containsAll(expected1.entrySet())); Map result2 = results.get(1).getMap(); - assertEquals(ImmutableMap.builder() + Map expected2 = ImmutableMap.builder() .put("ti_nm", "name") .put("ti_id", "order_id") .put("e", "ti") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("aid", "cloudfront") .put("ti_cu", "currency") - .put("dtm", "123456") .put("ttm", "456789") .put("tz", "Etc/UTC") .put("ti_pr", "1.0") @@ -158,7 +193,9 @@ public void testEcommerceEvent() throws InterruptedException { .put("tv", Version.TRACKER) .put("ti_ca", "category") .put("ti_sk", "sku") - .build(), result2); + .build(); + + assertTrue(result2.entrySet().containsAll(expected2.entrySet())); } @Test @@ -166,32 +203,30 @@ public void testUnstructuredEventWithContext() throws InterruptedException { // When tracker.track(Unstructured.builder() .eventData(new SelfDescribingJson( - "payload", + "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") )) .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected = ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) .put("e", "ue") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"payload\",\"data\":{\"foo\":\"bar\"}}}") - .put("dtm", "123456") + .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}") .put("ttm", "456789") .put("aid", "cloudfront") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -199,30 +234,28 @@ public void testUnstructuredEventWithoutContext() throws InterruptedException { // When tracker.track(Unstructured.builder() .eventData(new SelfDescribingJson( - "payload", + "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "baær") )) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected = ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) - .put("eid", EXPECTED_EVENT_ID) .put("e", "ue") .put("tna", "AF003") .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"payload\",\"data\":{\"foo\":\"baær\"}}}") - .put("dtm", "123456") + .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"baær\"}}}") .put("ttm", "456789") .put("aid", "cloudfront") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -230,33 +263,31 @@ public void testUnstructuredEventWithoutTrueTimestamp() throws InterruptedExcept // When tracker.track(Unstructured.builder() .eventData(new SelfDescribingJson( - "payload", - ImmutableMap.of("foo", "baær") + "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", + ImmutableMap.of("foo", "bar") )) - .deviceCreatedTimestamp(123456) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected = ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) - .put("eid", EXPECTED_EVENT_ID) .put("e", "ue") .put("tna", "AF003") .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"payload\",\"data\":{\"foo\":\"baær\"}}}") - .put("dtm", "123456") + .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}") .put("aid", "cloudfront") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test public void testTrackPageView() throws InterruptedException { - tracker = new Tracker.TrackerBuilder(this.mockEmitter, "AF003", "cloudfront") + tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") .subject(new Subject.SubjectBuilder().build()) .base64(false) .build(); @@ -268,17 +299,14 @@ public void testTrackPageView() throws InterruptedException { .pageTitle("title") .referrer("referer") .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() - .put("dtm", "123456") + Map expected = ImmutableMap.builder() .put("ttm", "456789") .put("tz", "Etc/UTC") .put("e", "pv") @@ -286,12 +314,13 @@ public void testTrackPageView() throws InterruptedException { .put("tv", Version.TRACKER) .put("p", "srv") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("aid", "cloudfront") .put("refr", "referer") .put("url", "url") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -301,20 +330,15 @@ public void testTrackTwoEvents() throws InterruptedException { .pageUrl("url") .pageTitle("title") .referrer("referer") - .deviceCreatedTimestamp(123456) - .trueTimestamp(456789L) - .eventId("9783090a-dace-4c85-a75c-933b4596a6c5") + .trueTimestamp(123456L) .build()); - Thread.sleep(500); - - tracker.track(PageView.builder() - .pageUrl("url") - .pageTitle("title") - .referrer("referer") - .deviceCreatedTimestamp(123456) + tracker.track(Unstructured.builder() + .eventData(new SelfDescribingJson( + "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", + ImmutableMap.of("foo", "bar") + )) .trueTimestamp(456789L) - .eventId("39139d43-ea13-4163-8559-adea258bf9c4") .build()); // Then @@ -324,36 +348,34 @@ public void testTrackTwoEvents() throws InterruptedException { assertEquals(2, results.size()); Map result1 = results.get(0).getMap(); - assertEquals(ImmutableMap.builder() - .put("dtm", "123456") - .put("ttm", "456789") + Map expected1 = ImmutableMap.builder() + .put("ttm", "123456") .put("tz", "Etc/UTC") .put("e", "pv") .put("page", "title") .put("tv", Version.TRACKER) .put("p", "srv") - .put("eid", "9783090a-dace-4c85-a75c-933b4596a6c5") .put("tna", "AF003") .put("aid", "cloudfront") .put("refr", "referer") .put("url", "url") - .build(), result1); + .build(); + + assertTrue(result1.entrySet().containsAll(expected1.entrySet())); Map result2 = results.get(1).getMap(); - assertEquals(ImmutableMap.builder() - .put("dtm", "123456") + Map expected2 = ImmutableMap.builder() .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("e", "pv") - .put("page", "title") - .put("tv", Version.TRACKER) .put("p", "srv") - .put("eid", "39139d43-ea13-4163-8559-adea258bf9c4") + .put("tv", Version.TRACKER) + .put("e", "ue") .put("tna", "AF003") + .put("tz", "Etc/UTC") + .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}") .put("aid", "cloudfront") - .put("refr", "referer") - .put("url", "url") - .build(), result2); + .build(); + + assertTrue(result2.entrySet().containsAll(expected2.entrySet())); } @Test @@ -363,28 +385,26 @@ public void testTrackScreenView() throws InterruptedException { .name("name") .id("id") .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() - .put("dtm", "123456") + Map expected = ImmutableMap.builder() .put("ttm", "456789") .put("tz", "Etc/UTC") .put("e", "ue") .put("tv", Version.TRACKER) .put("p", "srv") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("aid", "cloudfront") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -393,27 +413,25 @@ public void testTrackScreenViewWithTimestamp() throws InterruptedException { tracker.track(ScreenView.builder() .name("name") .id("id") - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() - .put("dtm", "123456") + Map expected = ImmutableMap.builder() .put("ttm", "456789") .put("tz", "Etc/UTC") .put("e", "ue") .put("tv", Version.TRACKER) .put("p", "srv") - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("aid", "cloudfront") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -423,28 +441,26 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() throws Interrupt .name("name") .id("id") .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected = ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) .put("e", "ue") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("tz", "Etc/UTC") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") - .put("dtm", "123456") .put("ttm", "456789") .put("aid", "cloudfront") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -456,28 +472,26 @@ public void testTrackTiming() throws InterruptedException { .variable("variable") .timing(10) .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .build()); // Then Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected = ImmutableMap.builder() .put("p", "srv") .put("tv", Version.TRACKER) .put("e", "ue") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("tz", "Etc/UTC") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") - .put("dtm", "123456") .put("ttm", "456789") .put("aid", "cloudfront") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); } @Test @@ -494,9 +508,7 @@ public void testTrackTimingWithSubject() throws InterruptedException { .variable("variable") .timing(10) .customContext(contexts) - .deviceCreatedTimestamp(123456) .trueTimestamp(456789L) - .eventId(EXPECTED_EVENT_ID) .subject(s1) .build()); @@ -504,20 +516,21 @@ public void testTrackTimingWithSubject() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - assertEquals(ImmutableMap.builder() + Map expected = ImmutableMap.builder() .put("p", "srv") .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") .put("tv", Version.TRACKER) .put("e", "ue") .put("ip", "127.0.0.1") .put("co", EXPECTED_CONTEXTS) - .put("eid", EXPECTED_EVENT_ID) .put("tna", "AF003") .put("tz", "Etc/UTC") - .put("dtm", "123456") .put("ttm", "456789") .put("aid", "cloudfront") - .build(), result); + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); + } // --- Tracker Setter & Getter Tests @@ -538,17 +551,23 @@ public void testSetDefaultPlatform() { @Test public void testSetSubject() { + // Subject objects always have timezone set TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); + Subject s1 = new Subject.SubjectBuilder().build(); + s1.setLanguage("EN"); Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") .subject(s1) .build(); + Subject s2 = new Subject.SubjectBuilder().build(); s2.setColorDepth(24); tracker.setSubject(s2); + Map subjectPairs = new HashMap<>(); subjectPairs.put("tz", "Etc/UTC"); subjectPairs.put("cd", "24"); + assertEquals(subjectPairs, tracker.getSubject().getSubject()); } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 5c310e41..2953c7f1 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -25,9 +25,6 @@ import org.junit.Before; import org.junit.Test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.events.PageView; @@ -60,14 +57,10 @@ public int get(TrackerPayload payload) { } @Override - public String getUrl() { - return null; - } + public String getUrl() { return null; } @Override - public Object getHttpClient() { - return null; - } + public Object getHttpClient() { return null; } } // this class fails to "send" the first 4 requests @@ -87,19 +80,13 @@ public int post(SelfDescribingJson payload) { } @Override - public int get(TrackerPayload payload) { - return 0; - } + public int get(TrackerPayload payload) { return 0; } @Override - public String getUrl() { - return null; - } + public String getUrl() { return null; } @Override - public Object getHttpClient() { - return null; - } + public Object getHttpClient() { return null; } } @Before @@ -114,16 +101,15 @@ public void setUp() { @Test public void addToBuffer_withLess10Payloads_shouldNotEmptyBuffer() throws InterruptedException { - List payloads = createPayloads(2); - for (TrackerPayload payload : payloads) { - emitter.add(payload); - } + TrackerPayload payload = createPayload(); + boolean result = emitter.add(payload); Thread.sleep(500); + Assert.assertTrue(result); Assert.assertFalse(mockHttpClientAdapter.isPostCalled); - Assert.assertEquals(2, emitter.getBuffer().size()); - Assert.assertEquals(payloads, emitter.getBuffer()); + Assert.assertEquals(1, emitter.getBuffer().size()); + Assert.assertEquals(payload, emitter.getBuffer().get(0)); } @Test @@ -136,8 +122,6 @@ public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Interrupte Thread.sleep(500); Assert.assertTrue(mockHttpClientAdapter.isPostCalled); - @SuppressWarnings("unchecked") - List> capturedPayload = (List>) mockHttpClientAdapter.capturedPayload.getMap().get("data"); Assert.assertEquals(0, emitter.getBuffer().size()); Assert.assertEquals(1, mockHttpClientAdapter.postCounter); @@ -153,9 +137,10 @@ public void addToBuffer_doesNotAddEventIfBufferFull() { emitter.add(createPayload()); TrackerPayload differentPayload = createPayload(); - emitter.add(differentPayload); + boolean result = emitter.add(differentPayload); Assert.assertFalse(emitter.getBuffer().contains(differentPayload)); + Assert.assertFalse(result); } @Test @@ -358,10 +343,10 @@ private void assertPayload(List payloads, List Date: Mon, 14 Mar 2022 10:12:51 +0000 Subject: [PATCH 078/128] Update copyright notices to 2022 (close #312) * Add copyright notices to EventStore and InMemoryEventStore * Update copyright notices to 2022 * Update LICENSE copyright --- LICENSE | 4 ++-- build.gradle | 4 ++-- examples/benchmarking/build.gradle | 2 +- .../java/com/snowplowanalytics/TrackerBenchmark.java | 2 +- examples/simple-console/gradlew | 2 +- .../src/main/java/com/snowplowanalytics/Main.java | 2 +- .../test/java/com/snowplowanalytics/MainTest.java | 4 ++-- .../snowplow/tracker/DevicePlatform.java | 2 +- .../snowplowanalytics/snowplow/tracker/Subject.java | 2 +- .../snowplowanalytics/snowplow/tracker/Tracker.java | 2 +- .../snowplowanalytics/snowplow/tracker/Utils.java | 2 +- .../snowplow/tracker/constants/Constants.java | 2 +- .../snowplow/tracker/constants/Parameter.java | 2 +- .../snowplow/tracker/emitter/AbstractEmitter.java | 2 +- .../snowplow/tracker/emitter/BatchEmitter.java | 2 +- .../snowplow/tracker/emitter/BatchPayload.java | 2 +- .../snowplow/tracker/emitter/Emitter.java | 2 +- .../snowplow/tracker/emitter/EventStore.java | 12 ++++++++++++ .../snowplow/tracker/emitter/InMemoryEventStore.java | 12 ++++++++++++ .../snowplow/tracker/emitter/SimpleEmitter.java | 2 +- .../snowplow/tracker/events/AbstractEvent.java | 2 +- .../tracker/events/EcommerceTransaction.java | 2 +- .../tracker/events/EcommerceTransactionItem.java | 2 +- .../snowplow/tracker/events/Event.java | 2 +- .../snowplow/tracker/events/PageView.java | 2 +- .../snowplow/tracker/events/ScreenView.java | 2 +- .../snowplow/tracker/events/Structured.java | 2 +- .../snowplow/tracker/events/Timing.java | 2 +- .../snowplow/tracker/events/Unstructured.java | 2 +- .../tracker/http/AbstractHttpClientAdapter.java | 2 +- .../tracker/http/ApacheHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/HttpClientAdapter.java | 2 +- .../snowplow/tracker/http/OkHttpClientAdapter.java | 2 +- .../snowplow/tracker/payload/Payload.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJson.java | 2 +- .../snowplow/tracker/payload/TrackerParameters.java | 2 +- .../snowplow/tracker/payload/TrackerPayload.java | 2 +- .../snowplow/tracker/SubjectTest.java | 2 +- .../snowplow/tracker/TrackerTest.java | 2 +- .../snowplow/tracker/UtilsTest.java | 2 +- .../tracker/emitter/BatchEmitterBuilderTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterTest.java | 2 +- .../tracker/emitter/InMemoryEventStoreTest.java | 2 +- .../snowplow/tracker/http/HttpClientAdapterTest.java | 2 +- .../tracker/payload/SelfDescribingJsonTest.java | 2 +- .../snowplow/tracker/payload/TrackerPayloadTest.java | 2 +- 46 files changed, 71 insertions(+), 47 deletions(-) diff --git a/LICENSE b/LICENSE index b2d6fe1e..e0977a71 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021 Snowplow Analytics Ltd. + Copyright 2022 Snowplow Analytics Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/build.gradle b/build.gradle index 8fea99c3..f28ec1f0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -98,7 +98,7 @@ task generateSources { srcFile.parentFile.mkdirs() srcFile.write( """/* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/benchmarking/build.gradle b/examples/benchmarking/build.gradle index e00d4ad9..713d8895 100644 --- a/examples/benchmarking/build.gradle +++ b/examples/benchmarking/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java b/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java index c51b900f..460ed692 100644 --- a/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java +++ b/examples/benchmarking/src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/gradlew b/examples/simple-console/gradlew index 1b6c7873..f887d101 100755 --- a/examples/simple-console/gradlew +++ b/examples/simple-console/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015-2022 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 490c30dd..0ea771e8 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java index 30698085..01d5720d 100644 --- a/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java +++ b/examples/simple-console/src/test/java/com/snowplowanalytics/MainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -37,4 +37,4 @@ public void testGetUrlEmpty() { Main.getUrlFromArgs(new String[]{}); } -} \ No newline at end of file +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java index b457bfb9..4ca87887 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index 1b0ff8f3..b63789d9 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index abf41b58..bdd8ba39 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index a031e0d3..6bcafe8e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java index 0bf291b4..a57fee1c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index f00cf1a3..b4293244 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index 00779196..28218877 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index c13ea4fe..9002d34b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java index f561e1aa..088d65eb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index c9cb9b50..e04fbd37 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index 9a2eaa6a..c6debf99 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -1,3 +1,15 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ package com.snowplowanalytics.snowplow.tracker.emitter; import java.util.List; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index d5366da5..e3e63821 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -1,3 +1,15 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ package com.snowplowanalytics.snowplow.tracker.emitter; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index a6cd3bf8..f61b8e11 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 369bb3a1..06356d7a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 9151e014..87ba492a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index 37a89cbc..e3875ff8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index b9909913..7055a473 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index 87da01f4..cdd7de74 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 9b1bf1a0..45667d24 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index 06e7f282..ff437818 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index c005d158..d13ffd6c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index 37174422..9c927790 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index 0623966e..451d5aa7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 121e24a4..5620d88a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java index bbf00bdf..e971d6a7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 4c74e94d..98dc6026 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java index e6febee3..8cd60503 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index a41285ea..eb0a99dd 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java index 66d5be3e..efa4e30a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 21af7f98..82e419a8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index b2850d2c..9ef9352e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 3cb441da..6a39be08 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index 63906c5e..214ba30e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java index 2bb49acc..a1159ac4 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 2953c7f1..a4b112ed 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java index 33a7d843..36e8f953 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index aff33299..7e61050c 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java index 77565065..2c763e94 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java index 0d566440..6b2db9eb 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. From f74edc7315b46800c53d0c1456928715bdc11f02 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 14 Mar 2022 10:13:12 +0000 Subject: [PATCH 079/128] Update junit and jackson-databind (close #294) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f28ec1f0..558a40f4 100644 --- a/build.gradle +++ b/build.gradle @@ -73,14 +73,14 @@ dependencies { testImplementation 'org.slf4j:slf4j-simple:1.7.30' // Jackson JSON processor - api 'com.fasterxml.jackson.core:jackson-databind:2.11.0' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.1' // Preconditions api 'com.google.guava:guava:31.0-jre' // Testing libraries testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testCompileOnly 'junit:junit:4.13' + testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' From 2a95965140080118e261634f826bd3fa40b51608 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 14 Mar 2022 10:13:33 +0000 Subject: [PATCH 080/128] Deprecate SimpleEmitter (close #309) --- .../snowplow/tracker/emitter/AbstractEmitter.java | 1 + .../snowplowanalytics/snowplow/tracker/emitter/Emitter.java | 4 ++-- .../snowplow/tracker/emitter/SimpleEmitter.java | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index 28218877..adf184b5 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -29,6 +29,7 @@ /** * AbstractEmitter class which contains common elements to * the emitters wrapped in a builder format. + * Note that SimpleEmitter has been deprecated. */ public abstract class AbstractEmitter implements Emitter { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index e04fbd37..3b8614df 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -32,7 +32,7 @@ public interface Emitter { /** * Customize the emitter batch size to any valid integer * greater than zero. - * - Will only affect the BatchEmitter + * Will only affect the BatchEmitter * * @param batchSize number of events to collect before * sending @@ -46,7 +46,7 @@ public interface Emitter { /** * Gets the Emitter Batch Size - * - Will always be 1 for SimpleEmitter + * Will always be 1 for SimpleEmitter. Note that SimpleEmitter has been deprecated. * * @return the batch size */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index f61b8e11..3f754027 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -24,7 +24,9 @@ /** * An emitter which sends events as soon as they are received via * GET requests. + * @deprecated Use the BatchEmitter, or create your own Emitter using the provided interface. */ +@Deprecated public class SimpleEmitter extends AbstractEmitter { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleEmitter.class); From 71ee001ae3dfca25d2dbb321aa697b616c485487 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 14 Mar 2022 10:33:59 +0000 Subject: [PATCH 081/128] Add Javadoc generation (close #137) * Add workflow for publishing javadocs * Empty commit to retrigger workflow * Allow empty commit for GH pages action * Remove line about empty commits * Add minor details to docstrings * Add javadoc for EventStore and InMemoryEventStore * Update javadocs for payloads and httpclientadapters * Add a test for tracking EcommerceTransactionItem * Improve EcommerceTransactionItem description * Add docstrings for Events * Update javadocs only on push to master --- .github/workflows/documentation.yml | 30 ++++++++++ build.gradle | 3 + .../snowplow/tracker/DevicePlatform.java | 3 + .../snowplow/tracker/Subject.java | 5 +- .../snowplow/tracker/Tracker.java | 27 +++++---- .../snowplow/tracker/constants/Constants.java | 2 +- .../snowplow/tracker/constants/Parameter.java | 4 ++ .../tracker/emitter/AbstractEmitter.java | 8 +-- .../tracker/emitter/BatchEmitter.java | 41 +++++++++---- .../tracker/emitter/BatchPayload.java | 8 ++- .../snowplow/tracker/emitter/Emitter.java | 1 + .../snowplow/tracker/emitter/EventStore.java | 35 +++++++++++- .../tracker/emitter/InMemoryEventStore.java | 57 ++++++++++++++++++- .../tracker/emitter/SimpleEmitter.java | 9 +-- .../tracker/events/AbstractEvent.java | 23 ++++---- .../tracker/events/EcommerceTransaction.java | 29 +++++++++- .../events/EcommerceTransactionItem.java | 29 ++++++---- .../snowplow/tracker/events/Event.java | 4 +- .../snowplow/tracker/events/PageView.java | 7 ++- .../snowplow/tracker/events/ScreenView.java | 11 +++- .../snowplow/tracker/events/Structured.java | 17 ++++-- .../snowplow/tracker/events/Timing.java | 8 ++- .../snowplow/tracker/events/Unstructured.java | 13 +++-- .../snowplow/tracker/payload/Payload.java | 10 ++-- .../tracker/payload/SelfDescribingJson.java | 23 +++++--- .../tracker/payload/TrackerParameters.java | 3 + .../tracker/payload/TrackerPayload.java | 16 ++++-- .../snowplow/tracker/TrackerTest.java | 42 ++++++++++++++ 28 files changed, 372 insertions(+), 96 deletions(-) create mode 100644 .github/workflows/documentation.yml diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..c697bad1 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,30 @@ +name: Documentation + +on: + push: + branches: [ master ] + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: Build + run: ./gradlew build + + - name: Deploy to GitHub Pages + if: success() + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/docs/javadoc diff --git a/build.gradle b/build.gradle index 558a40f4..3c81ed88 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,9 @@ task generateSources { */ package com.snowplowanalytics.snowplow.tracker; // DO NOT EDIT. AUTO-GENERATED. +/** +* The release version of the Snowplow Java tracker. +*/ public class Version { static final String TRACKER = "java-$project.version"; static final String VERSION = "$project.version"; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java index 4ca87887..f8578736 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/DevicePlatform.java @@ -13,6 +13,9 @@ package com.snowplowanalytics.snowplow.tracker; +/** + * The supported platform options for Tracker objects. + */ public enum DevicePlatform { Web { public String toString() { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index b63789d9..d058c847 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -115,6 +115,8 @@ public SubjectBuilder colorDepth(int depth) { } /** + * Note that timezone is set by default to the server's timezone + * (`TimeZone tz = Calendar.getInstance().getTimeZone().getID()`) * @param timezone a timezone string * @return itself */ @@ -236,7 +238,8 @@ public void setColorDepth(int depth) { } /** - * Sets the timezone parameter + * Sets the timezone parameter. Note that timezone is set by default to the server's timezone + * (`TimeZone tz = Calendar.getInstance().getTimeZone().getID()`); * * @param timezone a timezone string */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index bdd8ba39..c058e2ac 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -21,17 +21,17 @@ import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerParameters; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.*; +/** + * Allows tracking of Events. + */ public class Tracker { private Emitter emitter; private Subject subject; private final TrackerParameters parameters; - private static final Logger LOGGER = LoggerFactory.getLogger(Tracker.class); /** * Creates a new Snowplow Tracker. @@ -86,6 +86,9 @@ public TrackerBuilder subject(Subject subject) { } /** + * The devicePlatform the tracker is running on ({@link DevicePlatform}). + * The default is "srv", ServerSideApp. + * * @param platform The device platform the tracker is running on * @return itself */ @@ -116,6 +119,8 @@ public Tracker build() { // --- Setters /** + * Change the Emitter used to send events. + * * @param emitter a new emitter */ public void setEmitter(Emitter emitter) { @@ -142,14 +147,16 @@ public Emitter getEmitter() { } /** - * @return the Tracker Subject + * @return the Tracker-associated Subject */ public Subject getSubject() { return this.subject; } /** - * @return the tracker version that was set + * The Java tracker release version, e.g. 0.12.0. + * + * @return the tracker version */ public String getTrackerVersion() { return this.parameters.getTrackerVersion(); @@ -163,7 +170,7 @@ public String getNamespace() { } /** - * @return the trackers set Application ID + * @return the tracker Application ID */ public String getAppId() { return this.parameters.getAppId(); @@ -177,7 +184,7 @@ public boolean getBase64Encoded() { } /** - * @return the Tracker platform + * @return the Tracker platform, e.g. "srv" */ public DevicePlatform getPlatform() { return this.parameters.getPlatform(); @@ -193,8 +200,8 @@ public TrackerParameters getParameters() { // --- Event Tracking Functions /** - * Handles tracking the different types of events that - * the Tracker can encounter. + * Handles tracking the different types of events. + * * A TrackerPayload object - or more than one, in the case of eCommerceTransaction events - * will be created from the Event. This is passed to the configured Emitter. * If the event was successfully added to the Emitter buffer for sending, @@ -214,7 +221,7 @@ public List track(Event event) { // a list because Ecommerce events become multiple Payloads List processedEvents = eventTypeSpecificPreProcessing(event); for (Event processedEvent : processedEvents) { - // Event ID (eid) and device_created_timestamp (dtm) are generated when + // Event ID (eid) and device_created_timestamp (dtm) are generated now when // the TrackerPayload is created TrackerPayload payload = (TrackerPayload) processedEvent.getPayload(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java index a57fee1c..514dafc1 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java @@ -13,7 +13,7 @@ package com.snowplowanalytics.snowplow.tracker.constants; /** - * Constants which apply to schemas, event types + * Constants that apply to schemas, event types * and sending protocols. */ public class Constants { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index b4293244..abc01458 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -13,6 +13,10 @@ package com.snowplowanalytics.snowplow.tracker.constants; +/** + * More constants that define the event properties, which apply to schemas, event types + * and sending protocols. + */ public class Parameter { // General public static final String SCHEMA = "schema"; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index adf184b5..cdd013da 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -59,7 +59,7 @@ public T requestExecutorService(final ScheduledExecutorService executorService) } /** - * Adds the HttpClientAdapter to the AbstractEmitter + * Adds a custom HttpClientAdapter to the AbstractEmitter. The default is OkHttpClientAdapter. * * @param httpClientAdapter the adapter to use * @return itself @@ -70,7 +70,7 @@ public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { } /** - * Sets the Thread Count for the ExecutorService + * Sets the Thread Count for the ScheduledExecutorService. The default is 50. * * @param threadCount the size of the thread pool * @return itself @@ -81,8 +81,8 @@ public T threadCount(final int threadCount) { } /** - * Sets the emitter url for when a httpClientAdapter is not specified - * Will be used to create the default OkHttpClientAdapter. + * Sets the emitter url for when a httpClientAdapter is not specified. + * It will be used to create the default OkHttpClientAdapter. * * @param collectorUrl the url for the default httpClientAdapter * @return itself diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 9002d34b..a183bb3c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -29,8 +29,18 @@ import org.slf4j.LoggerFactory; /** - * An emitter that emit a batch of events in a single call - * It uses the post method of underlying http adapter + * An emitter that emits a batch of events in a single HTTP request. + * It uses the POST method of the underlying HTTP adapter. + * + * When a new event (TrackerPayload) is received and added to the buffer, the BatchEmitter checks the + * number of buffered events. If it is equal to or greater than the `batchSize`, an attempt is made to send + * a batch of events as one request. Events are sent asynchronously. + * + * If the request is unsuccessful, the events are returned to the buffer. A delay is introduced for all + * event sending attempts. This increases exponentially until a request succeeds, when it is reset to 0. + * Retry will continue indefinitely. + * + * If the buffer becomes full due to network problems, newer events will be lost. */ public class BatchEmitter extends AbstractEmitter implements Closeable { @@ -48,7 +58,9 @@ public static abstract class Builder> extends AbstractEmitt private EventStore eventStore; /** - * @param batchSize The count of events to buffer before sending + * The default batch size is 50. + * + * @param batchSize The count of events to send in one HTTP request * @return itself */ public T batchSize(final int batchSize) { @@ -57,6 +69,8 @@ public T batchSize(final int batchSize) { } /** + * The default EventStore is InMemoryEventStore. + * * @param eventStore The EventStore to use * @return itself */ @@ -66,6 +80,9 @@ public T eventStore(final EventStore eventStore) { } /** + * The default buffer capacity is Integer.MAX_VALUE. Your application would likely run out + * of memory before buffering this many events. When the buffer is full, new events are lost. + * * @param bufferCapacity The maximum capacity of the default InMemoryEventStore event buffer * @return itself */ @@ -106,11 +123,14 @@ protected BatchEmitter(final Builder builder) { } /** - * Adds a TrackerPayload to the concurrent queue buffer + * Adds a TrackerPayload to the EventStore buffer. + * If the buffer is full, the payload will be lost. + * *

* Implementation note: As a side effect it triggers an Emitter thread to emit a batch of events. * - * @param payload a payload + * @param payload a TrackerPayload + * @return whether the payload has been successfully added to the buffer. */ @Override public boolean add(final TrackerPayload payload) { @@ -130,7 +150,7 @@ public boolean add(final TrackerPayload payload) { } /** - * Forces all the payloads currently in the buffer to be sent immediately + * Forces all the payloads currently in the buffer to be sent immediately, as a single request. */ @Override public void flushBuffer() { @@ -138,7 +158,7 @@ public void flushBuffer() { } /** - * Returns List of Payloads that are in the buffer. + * Returns a List of Payloads that are in the buffer. * * @return the buffered events */ @@ -150,7 +170,7 @@ public List getBuffer() { /** * Customize the emitter batch size to any valid integer greater than zero. * - * @param batchSize number of events to collect before sending + * @param batchSize number of events to send in one request */ @Override public void setBatchSize(final int batchSize) { @@ -159,7 +179,7 @@ public void setBatchSize(final int batchSize) { } /** - * Gets the Emitter batch Size + * Gets the Emitter `batchSize` * * @return the batch size */ @@ -234,7 +254,8 @@ private SelfDescribingJson getFinalPost(final List events) { } /** - * On close, attempt to send all remaining events. + * Attempt to send all remaining events, then shut down the ExecutorService. + * *

* Implementation note: Be aware that calling `close()` * has a side-effect of shutting down the Emitter ScheduledExecutorService. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java index 088d65eb..f84f817e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java @@ -16,14 +16,16 @@ import java.util.List; - +/** + * A wrapper for a number of TrackerPayloads. + */ public class BatchPayload { private final Long batchId; private final List payloads; - public BatchPayload(Long payloadId, List payloads) { - this.batchId = payloadId; + public BatchPayload(Long batchId, List payloads) { + this.batchId = batchId; this.payloads = payloads; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index 3b8614df..b2b21a08 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -26,6 +26,7 @@ public interface Emitter { * we have reached the buffer limit yet. * * @param payload a payload to be emitted + * @return if the payload was added to the buffer */ boolean add(TrackerPayload payload); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index c6debf99..59cc03be 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -16,15 +16,46 @@ import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +/** + * EventStore interface. For buffering events in the Emitter. + */ public interface EventStore { + /** + * Add TrackerPayload to buffer. + * + * @param trackerPayload the payload to add + * @return success or not + */ boolean addEvent(TrackerPayload trackerPayload); - BatchPayload getEventsBatch(int numberToRemove); - + /** + * Remove some TrackerPayloads from the buffer. + * + * @param numberToGet how many payloads to get + * @return a BatchPayload wrapper + */ + BatchPayload getEventsBatch(int numberToGet); + + /** + * Get a copy of all the TrackerPayloads in the buffer. + * + * @return List of all the stored events + */ List getAllEvents(); + /** + * Finish processing events after a request has been made. + * + * @param successfullySent if the batch of events was successfully sent + * @param batchId the ID of the batch of events + */ void cleanupAfterSendingAttempt(boolean successfullySent, long batchId); + /** + * Get the current size of the buffer. + * + * @return number of events currently in the buffer + */ int size(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index e3e63821..672cd183 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -21,26 +21,59 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicLong; +/** + * Buffers events (as TrackerPayloads) in memory for sending via the BatchEmitter. + * + * The TrackerPayloads are stored in a queue. When the BatchEmitter calls {@link #getEventsBatch(int)}, + * the chosen number of TrackerPayloads are removed from the queue. The batch is added to a map of payloads + * that are currently being sent, and wrapped as a BatchPayload. This BatchPayload wrapper is returned to the + * Emitter. + * + * If the POST request is successful, the payloads are deleted from the map. + * If not, they are removed from the map and reinserted into the queue to be sent again. + */ public class InMemoryEventStore implements EventStore { private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryEventStore.class); private final AtomicLong batchId = new AtomicLong(1); - public final LinkedBlockingDeque eventBuffer; - public final ConcurrentHashMap> eventsBeingSent = new ConcurrentHashMap<>(); + private final LinkedBlockingDeque eventBuffer; + private final ConcurrentHashMap> eventsBeingSent = new ConcurrentHashMap<>(); + /** + * Make a new InMemoryEventStore with default queue capacity (Integer.MAX_VALUE). + */ public InMemoryEventStore() { eventBuffer = new LinkedBlockingDeque<>(); } + /** + * Make a new InMemoryEventStore with user-set queue capacity. + * + * @param bufferCapacity the maximum number of events to buffer at once + */ public InMemoryEventStore(int bufferCapacity) { eventBuffer = new LinkedBlockingDeque<>(bufferCapacity); } + /** + * Add TrackerPayload to buffer. Returns false if the buffer was full. + * Note that the event is lost in this case. + * + * @param trackerPayload the payload to add + * @return success or not + */ @Override public boolean addEvent(TrackerPayload trackerPayload) { return eventBuffer.offer(trackerPayload); } + /** + * Remove some TrackerPayloads from the buffer. They are wrapped as a BatchPayload to return, + * and also stored in a separate collection inside InMemoryEventStore until the result of their POST request is known. + * + * @param numberToGet how many payloads to get + * @return a BatchPayload wrapper + */ @Override public BatchPayload getEventsBatch(int numberToGet) { List eventsToSend = new ArrayList<>(); @@ -54,6 +87,14 @@ public BatchPayload getEventsBatch(int numberToGet) { return batchedEvents; } + /** + * Finish processing events after a request has been made. If the request was successful, + * the events are deleted from the InMemoryEventStore. If not, they are reinserted at the beginning + * of the buffer queue for another attempt. + * + * @param successfullySent if the batch of events was successfully sent + * @param batchId the ID of the batch of events + */ @Override public void cleanupAfterSendingAttempt(boolean successfullySent, long batchId) { // Events that successfully sent are deleted from the pending buffer @@ -74,12 +115,24 @@ public void cleanupAfterSendingAttempt(boolean successfullySent, long batchId) { } } + /** + * Get a copy of all the TrackerPayloads in the buffer. This does not include any events + * currently being sent by the BatchEmitter. + * + * @return List of all the stored events + */ @Override public List getAllEvents() { TrackerPayload[] events = eventBuffer.toArray(new TrackerPayload[0]); return Arrays.asList(events); } + /** + * Get the current size of the buffer. This does not include any events + * currently being sent by the BatchEmitter. + * + * @return number of events currently in the buffer + */ @Override public int size() { return eventBuffer.size(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java index 3f754027..8f81dce0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java @@ -22,8 +22,8 @@ import com.snowplowanalytics.snowplow.tracker.constants.Parameter; /** - * An emitter which sends events as soon as they are received via - * GET requests. + * An emitter which sends events one-by-one as soon as they are received, via + * GET requests. The events are sent asynchronously. * @deprecated Use the BatchEmitter, or create your own Emitter using the provided interface. */ @Deprecated @@ -53,9 +53,10 @@ protected SimpleEmitter(final Builder builder) { } /** - * Adds an event to the buffer and instantly sends it + * Adds an event and instantly tries to send it. * * @param payload a payload + * @return true */ @Override public boolean add(TrackerPayload payload) { @@ -67,7 +68,7 @@ public boolean add(TrackerPayload payload) { } /** - * Sends buffered events, but SimpleEmitter does not buffer events + * Sends all the buffered events, but SimpleEmitter does not buffer events. * So has no effect */ @Override diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 06356d7a..2c934d72 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -22,26 +22,26 @@ // This library import com.snowplowanalytics.snowplow.tracker.Subject; -import com.snowplowanalytics.snowplow.tracker.Utils; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.payload.Payload; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; /** - * Base AbstractEvent class which contains common - * elements to all events: - * - Custom Context: list of custom contexts or null - * - Timestamp: user defined event timestamp or 0 - * - Event Id: a unique id for the event - * - Subject: a unique Subject for the event + * Base AbstractEvent class which contains + * elements that can be set in all events. These are context, trueTimestamp, and Subject. + * + * Context is a list of custom SelfDescribingJson entities. + * TrueTimestamp is a user-defined timestamp. + * Subject is an event-specific Subject. Its fields will override those of the + * Tracker-associated Subject, if present. */ public abstract class AbstractEvent implements Event { protected final List context; /** - * The true timestamp may be null if none is set. + * The trueTimestamp may be null if none is set. */ protected Long trueTimestamp; protected final Subject subject; @@ -55,9 +55,9 @@ public static abstract class Builder> { protected abstract T self(); /** - * Adds a list of custom contexts. + * Adds a list of custom context entities. * - * @param context the list of contexts + * @param context the list of entities * @return itself */ public T customContext(List context) { @@ -78,7 +78,8 @@ public T trueTimestamp(Long timestamp) { } /** - * A custom subject for the event. + * A custom subject for the event. Its fields will override those of the + * Tracker-associated Subject, if present. * * @param subject the eventId * @return itself diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 87ba492a..814bc7bd 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -25,6 +25,24 @@ import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +/** + * Constructs an EcommerceTransaction event object. + *

+ * Implementation note: EcommerceTransaction/EcommerceTransactionItem uses a legacy design. + * We aim to deprecate it eventually. We advise using Unstructured events instead, and attaching the items + * as entities. + * + * The specific items purchased in the transaction must be added as EcommerceTransactionItem objects. + * This event type is different from the others in that it will generate more than one tracked event. + * There will be one "transaction" ("tr") event, and one "transaction item" ("ti") event for every + * EcommerceTransactionItem included in the EcommerceTransaction. + * + * To link the "transaction" and "transaction item" events, we recommend using the same orderId for the + * EcommerceTransaction and all attached EcommerceTransactionItems. + * + * To use the Currency Conversion pipeline enrichment, the currency string must be + * a valid Open Exchange Rates value. + */ public class EcommerceTransaction extends AbstractEvent { private final String orderId; @@ -133,6 +151,9 @@ public T currency(String currency) { } /** + * Provide a list of EcommerceTransactionItems. + * An empty list is valid, but probably not very useful. + * * @param items The items in the transaction * @return itself */ @@ -142,6 +163,9 @@ public T items(List items) { } /** + * Provide EcommerceTransactionItems directly, without explicitly adding them + * to a list beforehand. + * * @param itemArgs The items as a varargs argument * @return itself */ @@ -190,8 +214,7 @@ protected EcommerceTransaction(Builder builder) { } /** - * Returns a TrackerPayload which can be stored into - * the local database. + * Returns a TrackerPayload which can be passed to an Emitter. * * @return the payload to be sent. */ @@ -213,7 +236,7 @@ public TrackerPayload getPayload() { } /** - * The list of Transaction Items passed with the event. + * The list of EcommerceTransactionItems passed with the event. * * @return the items. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index e3875ff8..c601df7b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -21,6 +21,23 @@ import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +/** + * Constructs an EcommerceTransactionItem object. + *

+ * Implementation note: EcommerceTransaction/EcommerceTransactionItem uses a legacy design. + * We aim to deprecate it eventually. We advise using Unstructured events instead, and attaching the items + * as entities. + * + * EcommerceTransactionItems were designed for attaching data about purchased items to a + * EcommerceTransaction event. They can technically be sent as events in their own right, but this is + * not supported. + * + * To link the "transaction" and "transaction item" events, we recommend using the same orderId for the + * EcommerceTransaction and all attached EcommerceTransactionItems. + * + * To use the Currency Conversion pipeline enrichment, the currency string must be + * a valid Open Exchange Rates value. + */ public class EcommerceTransactionItem extends AbstractEvent { private final String itemId; @@ -42,7 +59,7 @@ public static abstract class Builder> extends AbstractEvent private String currency; /** - * @param itemId Item ID + * @param itemId Item ID - ideally the same as the EcommerceTransaction orderId * @return itself */ public T itemId(String itemId) { @@ -141,15 +158,7 @@ protected EcommerceTransactionItem(Builder builder) { } /** - * @param timestamp the new timestamp - */ - public void setTrueTimestamp(long timestamp) { - this.trueTimestamp = timestamp; - } - - /** - * Returns a TrackerPayload which can be stored into - * the local database. + * Returns a TrackerPayload which can be passed to an Emitter. * * @return the payload to be sent. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 7055a473..0c0ed380 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -24,7 +24,7 @@ public interface Event { /** - * @return the events custom context + * @return the event's custom context */ List getContext(); @@ -34,7 +34,7 @@ public interface Event { Long getTrueTimestamp(); /** - * @return the event subject + * @return the event-associated Subject */ Subject getSubject(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index cdd7de74..8c9c7df7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -22,6 +22,8 @@ /** * Constructs a PageView event object. + * + * When tracked, generates a "pv" or "page_view" event. */ public class PageView extends AbstractEvent { @@ -54,7 +56,7 @@ public T pageTitle(String pageTitle) { } /** - * @param referrer Referrer of the page + * @param referrer Referrer URL of the page * @return itself */ public T referrer(String referrer) { @@ -91,8 +93,7 @@ protected PageView(Builder builder) { } /** - * Returns a TrackerPayload which can be stored into - * the local database. + * Returns a TrackerPayload which can be passed to an Emitter. * * @return the payload to be sent. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 45667d24..91cbd2c6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -17,10 +17,14 @@ import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import java.util.LinkedHashMap; +/** + * Constructs a ScreenView event object. + * + * When tracked, generates an "unstructured" or "ue" event. + */ public class ScreenView extends AbstractEvent { private final String name; @@ -32,7 +36,7 @@ public static abstract class Builder> extends AbstractEvent private String id; /** - * @param name The name of the screen view event + * @param name The (human-readable) name of the screen view * @return itself */ public T name(String name) { @@ -76,7 +80,8 @@ protected ScreenView(Builder builder) { } /** - * Return the payload wrapped into a SelfDescribingJson. + * Return the payload wrapped into a SelfDescribingJson. When a ScreenView is tracked, + * the Tracker creates and tracks an Unstructured event from this SelfDescribingJson. * * @return the payload as a SelfDescribingJson. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index ff437818..6e0f0d6f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -22,6 +22,16 @@ /** * Constructs a Structured event object. + * + * This event type is provided to be roughly equivalent to Google Analytics-style events. + * Note that it is not automatically clear what data should be placed in what field. + * To aid data quality and modeling, agree on business-wide definitions when designing + * your tracking strategy. + * + * We recommend using Unstructured - fully custom - events instead. + * + * When tracked, generates a "struct" or "se" event. + * */ public class Structured extends AbstractEvent { @@ -49,7 +59,7 @@ public T category(String category) { } /** - * @param action The event itself + * @param action Describes what happened in the event * @return itself */ public T action(String action) { @@ -58,7 +68,7 @@ public T action(String action) { } /** - * @param label Refer to the object the action is performed on + * @param label Refers to the object the action is performed on * @return itself */ public T label(String label) { @@ -117,8 +127,7 @@ protected Structured(Builder builder) { } /** - * Returns a TrackerPayload which can be stored into - * the local database. + * Returns a TrackerPayload which can be passed to an Emitter. * * @return the payload to be sent. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index d13ffd6c..fdcdf4b2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -23,6 +23,11 @@ import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; +/** + * Constructs a Timing event object. + * + * When tracked, generates an "unstructured" or "ue" event. + */ public class Timing extends AbstractEvent { private final String category; @@ -106,7 +111,8 @@ protected Timing(Builder builder) { } /** - * Return the payload wrapped into a SelfDescribingJson. + * Return the payload wrapped into a SelfDescribingJson. When a Timing event is tracked, + * the Tracker creates and tracks an Unstructured event from this SelfDescribingJson. * * @return the payload as a SelfDescribingJson. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index 9c927790..25534349 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -23,6 +23,11 @@ /** * Constructs an Unstructured event object. + * + * This is a customisable event type which allows you to track anything describable + * by a JsonSchema. + * + * When tracked, generates an "unstructured" or "ue" event. */ public class Unstructured extends AbstractEvent { @@ -34,9 +39,8 @@ public static abstract class Builder> extends AbstractEvent private SelfDescribingJson eventData; /** - * @param selfDescribingJson The properties of the event. Has two field: - * A "data" field containing the event properties and - * A "schema" field identifying the schema against which the data is validated + * @param selfDescribingJson The properties of the event. Has two fields: "data", containing the event properties, + * and "schema", identifying the schema against which the data is validated * @return itself */ public T eventData(SelfDescribingJson selfDescribingJson) { @@ -77,8 +81,7 @@ public void setBase64Encode(boolean base64Encode) { } /** - * Returns a TrackerPayload which can be stored into - * the local database. + * Returns a TrackerPayload which can be passed to an Emitter. * * @return the payload to be sent. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java index 8cd60503..4014fc05 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java @@ -21,9 +21,9 @@ public interface Payload { /** - * Add a key-value pair to the payload: - * - Checks that the key is not null or empty - * - Checks that the value is not null or empty + * Add a key-value pair to the payload. + * + * It checks that neither the key nor value is null or empty. * * @param key The parameter key * @param value The parameter value as a String @@ -31,8 +31,8 @@ public interface Payload { void add(String key, String value); /** - * Add all the mappings from the specified map. The effect is the equivalent to that of calling: - * - add(String key, String value) for each key value pair. + * Add all the mappings from the specified map. The effect is the equivalent to that of calling + * {@link #add(String, String)} for each key value pair. * * @param map Key-Value pairs to be stored in this payload */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index eb0a99dd..f886ba1a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -24,9 +24,9 @@ import com.snowplowanalytics.snowplow.tracker.constants.Parameter; /** - * Builds a SelfDescribingJson object which can contain two fields: - * - Schema: the JsonSchema path for this Json - * - Data: the data for this Json + * Builds a SelfDescribingJson object. SelfDescribingJson must contain only two fields, schema and data. + * + * Schema is the JsonSchema path for this Json. Data is the data. */ public class SelfDescribingJson implements Payload { @@ -35,7 +35,7 @@ public class SelfDescribingJson implements Payload { /** * Creates a SelfDescribingJson with only a Schema - * String and an empty data map. + * String and an empty data map. Data can be added later using {@link #setData(Object)}. * * @param schema the schema string */ @@ -47,6 +47,11 @@ public SelfDescribingJson(String schema) { * Creates a SelfDescribingJson with a Schema and a * TrackerPayload object. * + * Note that TrackerPayload objects are initialised with an eventId UUID and + * timestamp (deviceCreatedTimestamp), as they are the basis for sending events. + * Therefore, your SelfDescribingJson data will contain the keys "eid" and "dtm". + * This is unlikely to be what you want. + * * @param schema the schema string * @param data a TrackerPayload object to be embedded as * the data @@ -59,7 +64,7 @@ public SelfDescribingJson(String schema, TrackerPayload data) { /** * Creates a SelfDescribingJson with a Schema and a * SelfDescribingJson object. This can be used to - * nest SDJs inside of each other. + * nest SDJs inside each other. * * @param schema the schema string * @param data a SelfDescribingJson object to be embedded as @@ -96,8 +101,12 @@ public SelfDescribingJson setSchema(String schema) { } /** - * Adds data to the SelfDescribingJson - * - Accepts a TrackerPayload object + * Adds data to the SelfDescribingJson from a TrackerPayload object. + * + * Note that TrackerPayload objects are initialised with an eventId UUID and + * timestamp (deviceCreatedTimestamp), as they are the basis for sending events. + * Therefore, your SelfDescribingJson data will contain the keys "eid" and "dtm". + * This is unlikely to be what you want. * * @param data the data to be added to the SelfDescribingJson * @return this SelfDescribingJson diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java index efa4e30a..260b27a3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -14,6 +14,9 @@ import com.snowplowanalytics.snowplow.tracker.DevicePlatform; +/** + * A wrapper for Tracker properties. + */ public class TrackerParameters { private final String trackerVersion; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index 82e419a8..d3328e11 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -23,8 +23,14 @@ import com.snowplowanalytics.snowplow.tracker.Utils; /** - * Returns a standard Tracker Payload consisting of - * many key - pair values. + * A TrackerPayload stores a map of key - pair values. + * + * When the Emitter attempts to send a TrackerPayload, these pairs are extracted + * and added to the HTTP request (via a SelfDescribingJson). + * The deviceSentTimestamp ("stm") is added at that point. + * + * EventId and deviceCreatedTimestamp are added to the internal map at + * TrackerPayload initialization. */ public class TrackerPayload implements Payload { @@ -51,8 +57,8 @@ public Long getDeviceCreatedTimestamp() { } /** - * Add a key-value pair to the payload: - Checks that the key is not null or - * empty - Checks that the value is not null or empty + * Add a key-value pair to the payload. + * Checks that neither the key nor the value are null or empty. * * @param key The parameter key * @param value The parameter value as a String @@ -73,7 +79,7 @@ public void add(final String key, final String value) { /** * Add all the mappings from the specified map. The effect is the equivalent to - * that of calling: - add(String key, String value) for each key value pair. + * that of calling {@link #add(String, String)} for each key value pair. * * @param map Key-Value pairs to be stored in this payload */ diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 6a39be08..2d234fb7 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -198,6 +198,48 @@ public void testEcommerceEvent() throws InterruptedException { assertTrue(result2.entrySet().containsAll(expected2.entrySet())); } + @Test + public void testEcommerceTransactionItemAlone() throws InterruptedException { + // Although surprising, EcommerceTransactionItems are valid events and + // can be sent separately from EcommerceTransactions. + + tracker.track(EcommerceTransactionItem.builder() + .itemId("order_id") + .sku("sku") + .price(1.0) + .quantity(2) + .name("name") + .category("category") + .currency("currency") + .customContext(contexts) + .trueTimestamp(456789L) + .build()); + + // Then + Thread.sleep(500); + + Map result = mockEmitter.eventList.get(0).getMap(); + Map expected = ImmutableMap.builder() + .put("ti_nm", "name") + .put("ti_id", "order_id") + .put("e", "ti") + .put("co", EXPECTED_CONTEXTS) + .put("tna", "AF003") + .put("aid", "cloudfront") + .put("ti_cu", "currency") + .put("ttm", "456789") + .put("tz", "Etc/UTC") + .put("ti_pr", "1.0") + .put("ti_qu", "2") + .put("p", "srv") + .put("tv", Version.TRACKER) + .put("ti_ca", "category") + .put("ti_sk", "sku") + .build(); + + assertTrue(result.entrySet().containsAll(expected.entrySet())); + } + @Test public void testUnstructuredEventWithContext() throws InterruptedException { // When From 4c8931570962d9d3287f16a711d425f48ee51116 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 17 Mar 2022 16:40:57 +0000 Subject: [PATCH 082/128] Choose HTTP response codes not to retry (close #316) * Set response codes for no retry * Don't add retry delay after a specified no retry code * Correctly retry after sending attempt catch * Return null from getBatchEvents() if there aren't enough events --- .../tracker/emitter/BatchEmitter.java | 36 +++++++++-- .../tracker/emitter/BatchPayload.java | 4 ++ .../snowplow/tracker/emitter/EventStore.java | 4 +- .../tracker/emitter/InMemoryEventStore.java | 15 +++-- .../tracker/emitter/BatchEmitterTest.java | 63 ++++++++++++++++--- .../emitter/InMemoryEventStoreTest.java | 12 ++-- 6 files changed, 107 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index a183bb3c..8b91a2fe 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -50,12 +50,14 @@ public class BatchEmitter extends AbstractEmitter implements Closeable { private int batchSize; private final EventStore eventStore; private final AtomicLong retryDelay; + private final List fatalResponseCodes; public static abstract class Builder> extends AbstractEmitter.Builder { private int batchSize = 50; // Optional private int bufferCapacity = Integer.MAX_VALUE; private EventStore eventStore; + private List fatalResponseCodes; /** * The default batch size is 50. @@ -91,6 +93,19 @@ public T bufferCapacity(final int bufferCapacity) { return self(); } + /** + * Provide a denylist of HTTP response codes. Retry will not be attempted if one of these codes + * is received. The events in the request will be dropped, but the Emitter will continue trying + * to send as normal. + * + * @param fatalResponseCodes Event sending will not be retried on these codes + * @return itself + */ + public T fatalResponseCodes(final List fatalResponseCodes) { + this.fatalResponseCodes = fatalResponseCodes; + return self(); + } + public BatchEmitter build() { return new BatchEmitter(this); } @@ -120,6 +135,12 @@ protected BatchEmitter(final Builder builder) { eventStore = builder.eventStore; } retryDelay = new AtomicLong(0L); + + if (builder.fatalResponseCodes != null) { + fatalResponseCodes = builder.fatalResponseCodes; + } else { + fatalResponseCodes = new ArrayList<>(); + } } /** @@ -203,12 +224,12 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { BatchPayload batchedEvents = null; try { batchedEvents = eventStore.getEventsBatch(numberOfEvents); - List eventsInRequest = batchedEvents.getPayloads(); - if (eventsInRequest.size() == 0) { + if (batchedEvents == null || batchedEvents.size() == 0) { return; } + List eventsInRequest = batchedEvents.getPayloads(); final SelfDescribingJson post = getFinalPost(eventsInRequest); final int code = httpClientAdapter.post(post); @@ -216,10 +237,15 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { if (isSuccessfulSend(code)) { LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", eventsInRequest.size(), code); retryDelay.set(0L); - eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); + eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + + } else if (fatalResponseCodes.contains(code)) { + LOGGER.debug("BatchEmitter failed to send {} events. No retry for code {}: events dropped", eventsInRequest.size(), code); + eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + } else { LOGGER.error("BatchEmitter failed to send {} events: code: {}", eventsInRequest.size(), code); - eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); // exponentially increase retry backoff time after the first failure if (!retryDelay.compareAndSet(0, 50L)) { @@ -229,7 +255,7 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { } catch (Exception e) { LOGGER.error("BatchEmitter event sending error: {}", e.getMessage()); if (batchedEvents != null) { - eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); } } }; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java index f84f817e..cf39dd2d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java @@ -36,4 +36,8 @@ public Long getBatchId() { public List getPayloads() { return payloads; } + + public int size() { + return payloads.size(); + } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index 59cc03be..3fa80d13 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -47,10 +47,10 @@ public interface EventStore { /** * Finish processing events after a request has been made. * - * @param successfullySent if the batch of events was successfully sent + * @param needRetry if another attempt should be made to send the events * @param batchId the ID of the batch of events */ - void cleanupAfterSendingAttempt(boolean successfullySent, long batchId); + void cleanupAfterSendingAttempt(boolean needRetry, long batchId); /** * Get the current size of the buffer. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index 672cd183..a698a16b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -72,13 +72,18 @@ public boolean addEvent(TrackerPayload trackerPayload) { * and also stored in a separate collection inside InMemoryEventStore until the result of their POST request is known. * * @param numberToGet how many payloads to get - * @return a BatchPayload wrapper + * @return a BatchPayload wrapper, or null */ @Override public BatchPayload getEventsBatch(int numberToGet) { List eventsToSend = new ArrayList<>(); - eventBuffer.drainTo(eventsToSend, numberToGet); + synchronized (eventBuffer) { + if (eventBuffer.size() < numberToGet) { + return null; + } + eventBuffer.drainTo(eventsToSend, numberToGet); + } // The batch of events is wrapped as a BatchPayload // They're also added to the "pending" event buffer, the eventsBeingSent HashMap @@ -92,17 +97,17 @@ public BatchPayload getEventsBatch(int numberToGet) { * the events are deleted from the InMemoryEventStore. If not, they are reinserted at the beginning * of the buffer queue for another attempt. * - * @param successfullySent if the batch of events was successfully sent + * @param needRetry if true, move events back to the buffer instead of deleting * @param batchId the ID of the batch of events */ @Override - public void cleanupAfterSendingAttempt(boolean successfullySent, long batchId) { + public void cleanupAfterSendingAttempt(boolean needRetry, long batchId) { // Events that successfully sent are deleted from the pending buffer List events = eventsBeingSent.remove(batchId); // Events that didn't send are inserted at the head of the eventBuffer // for immediate resending. - if (!successfullySent) { + if (needRetry) { while (events.size() > 0) { TrackerPayload payloadToReinsert = events.remove(0); boolean result = eventBuffer.offerFirst(payloadToReinsert); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index a4b112ed..bd0bb05b 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -33,9 +33,10 @@ public class BatchEmitterTest { private MockHttpClientAdapter mockHttpClientAdapter; - private FailingHttpClientAdapter failingHttpClientAdapter; + private FlakyHttpClientAdapter flakyHttpClientAdapter; private BatchEmitter emitter; + // MockHttpClientAdapter always returns 200 public static class MockHttpClientAdapter implements HttpClientAdapter { public boolean isGetCalled = false; public boolean isPostCalled = false; @@ -65,7 +66,7 @@ public int get(TrackerPayload payload) { // this class fails to "send" the first 4 requests // but returns a successful result (200) subsequently - static class FailingHttpClientAdapter implements HttpClientAdapter { + static class FlakyHttpClientAdapter implements HttpClientAdapter { int failedPostCounter = 0; int successfulPostCounter = 0; @Override @@ -89,10 +90,29 @@ public int post(SelfDescribingJson payload) { public Object getHttpClient() { return null; } } + // This class always returns failure code 403 + static class FailingHttpClientAdapter implements HttpClientAdapter { + int failedPostCounter = 0; + @Override + public int post(SelfDescribingJson payload) { + failedPostCounter++; + return 403; + } + + @Override + public int get(TrackerPayload payload) { return 0; } + + @Override + public String getUrl() { return null; } + + @Override + public Object getHttpClient() { return null; } + } + @Before public void setUp() { mockHttpClientAdapter = new MockHttpClientAdapter(); - failingHttpClientAdapter = new FailingHttpClientAdapter(); + flakyHttpClientAdapter = new FlakyHttpClientAdapter(); emitter = BatchEmitter.builder() .httpClientAdapter(mockHttpClientAdapter) .batchSize(10) @@ -252,7 +272,7 @@ public void close_sendsEventsAndStopsThreads() throws InterruptedException { @Test public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedException { emitter = BatchEmitter.builder() - .httpClientAdapter(new FailingHttpClientAdapter()) + .httpClientAdapter(new FlakyHttpClientAdapter()) .batchSize(10) .build(); @@ -273,7 +293,7 @@ public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedExc @Test public void eventSendingFailureIncreasesBackoffTime() throws InterruptedException { emitter = BatchEmitter.builder() - .httpClientAdapter(failingHttpClientAdapter) + .httpClientAdapter(flakyHttpClientAdapter) .batchSize(1) .build(); @@ -288,11 +308,11 @@ public void eventSendingFailureIncreasesBackoffTime() throws InterruptedExceptio @Test public void successfulSendAfterFailureResetsBackoffTime() throws InterruptedException { - // the FailingHttpClientAdapter returns 500 for the first 4 requests + // the FlakyHttpClientAdapter returns 500 for the first 4 requests // then subsequently returns 200 - FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); + FlakyHttpClientAdapter flakyHttpClientAdapter = new FlakyHttpClientAdapter(); emitter = BatchEmitter.builder() - .httpClientAdapter(failingHttpClientAdapter) + .httpClientAdapter(flakyHttpClientAdapter) .batchSize(1) .threadCount(1) .build(); @@ -304,8 +324,33 @@ public void successfulSendAfterFailureResetsBackoffTime() throws InterruptedExce Thread.sleep(500); - Assert.assertEquals(2, failingHttpClientAdapter.successfulPostCounter); + Assert.assertEquals(2, flakyHttpClientAdapter.successfulPostCounter); + Assert.assertEquals(0, emitter.getRetryDelay()); + } + + @Test + public void noRetryAfterDenylistResponseCode() throws InterruptedException { + List noRetry = new ArrayList<>(); + noRetry.add(403); + + // the FailingHttpClientAdapter always returns 403 + FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(failingHttpClientAdapter) + .batchSize(2) + .fatalResponseCodes(noRetry) + .build(); + + List payloads = createPayloads(4); + for (TrackerPayload payload : payloads) { + emitter.add(payload); + } + + Thread.sleep(500); + + Assert.assertEquals(2, failingHttpClientAdapter.failedPostCounter); Assert.assertEquals(0, emitter.getRetryDelay()); + Assert.assertEquals(0, emitter.getBuffer().size()); } private TrackerPayload createPayload() { diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java index 36e8f953..214a94d2 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -58,13 +58,13 @@ public void getEventsFromStorage() { } @Test - public void getAllEventsIfAskedForMoreEventsThanAreStored() { + public void doNotGetEventsIfFewerPresentThanAskedFor() throws NullPointerException { eventStore.addEvent(trackerPayload); eventStore.addEvent(trackerPayload); - List events = eventStore.getEventsBatch(3).getPayloads(); + BatchPayload events = eventStore.getEventsBatch(3); - Assert.assertEquals(2, events.size()); + Assert.assertNull(events); } @Test @@ -75,7 +75,7 @@ public void putEventsBackInBufferIfFailedToSend() { Assert.assertEquals(0, eventStore.size()); - eventStore.cleanupAfterSendingAttempt(false, 1L); + eventStore.cleanupAfterSendingAttempt(true, 1L); Assert.assertEquals(2, eventStore.size()); } @@ -88,7 +88,7 @@ public void doNotPutEventsBackInBufferIfSent() { Assert.assertEquals(0, eventStore.size()); - eventStore.cleanupAfterSendingAttempt(true, 1L); + eventStore.cleanupAfterSendingAttempt(false, 1L); Assert.assertEquals(0, eventStore.size()); } @@ -106,7 +106,7 @@ public void dropNewerEventsOnFailureWhenBufferFull() { eventStore.addEvent(trackerPayload); eventStore.addEvent(trackerPayload); - eventStore.cleanupAfterSendingAttempt(false, 1L); + eventStore.cleanupAfterSendingAttempt(true, 1L); Assert.assertEquals(3, eventStore.size()); Assert.assertTrue(eventStore.getAllEvents().contains(differentPayload)); From f4dbc060707f685f28da25986b6775c0067d8bb0 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 24 Mar 2022 17:41:07 +0000 Subject: [PATCH 083/128] Prepare for 0.12.0 release * Remove unused imports in simple-console * Update version number * Note which Event.Builder methods are required * Add link to Javadocs to README * Update CHANGELOG --- CHANGELOG | 17 ++++++++++++++++ README.md | 20 +++++++++++++------ build.gradle | 2 +- .../main/java/com/snowplowanalytics/Main.java | 2 -- .../snowplow/tracker/Tracker.java | 7 ++++--- .../tracker/emitter/AbstractEmitter.java | 6 +++--- .../tracker/emitter/BatchEmitter.java | 2 +- .../tracker/events/AbstractEvent.java | 2 +- .../tracker/events/EcommerceTransaction.java | 18 +++++++++++++++++ .../events/EcommerceTransactionItem.java | 14 +++++++++++++ .../snowplow/tracker/events/PageView.java | 6 ++++++ .../snowplow/tracker/events/ScreenView.java | 4 ++++ .../snowplow/tracker/events/Structured.java | 10 ++++++++++ .../snowplow/tracker/events/Timing.java | 8 ++++++++ .../snowplow/tracker/events/Unstructured.java | 2 ++ .../tracker/payload/SelfDescribingJson.java | 2 +- .../snowplow/tracker/TrackerTest.java | 2 +- 17 files changed, 105 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c5c176e4..e55776f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,20 @@ +Java 0.12.0 (2022-03-24) +----------------------- +Choose HTTP response codes not to retry (#316) +Add Javadoc generation (#137) +Deprecate SimpleEmitter (#309) +Update junit and jackson-databind (#294) +Update copyright notices to 2022 (#312) +Return eventId from Tracker.track() (#304) +Add retry to in-memory storage system (#156) +Rename bufferSize to batchSize (#306) +Add benchmarking tests (#300) +Update simple-console example (#295) +Provide method for stopping Tracker executorService (#297) +Refactor TrackerEvents for event payload creation (#291) +Extract event storage from Emitter (#290) +Attribute community contributions in changelog (#289) + Java 0.11.0 (2021-12-14) ----------------------- Remove logging of user supplied values (#286) diff --git a/README.md b/README.md index 1a4ba234..4a9191fc 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ With this tracker you can collect event data from your Java-based desktop and se ## Find out more -| Snowplow Docs | Contributing | -|---------------------------------|-----------------------------------| -| ![i1][techdocs-image] | ![i4][contributing-image] | -| **[Snowplow Docs][techdocs]** | **[Contributing](CONTRIBUTING.md)** | +| Snowplow Docs | API Docs | Contributing | +|-------------------------------|-----------|-----------------------------------| +| ![i1][techdocs-image] | ![i1][techdocs-image] | ![i4][contributing-image] | +| **[Snowplow Docs][techdocs]** | **[Javadoc Docs][apidocs]** | **[Contributing](CONTRIBUTING.md)** | ## Maintainer Quickstart @@ -33,15 +33,22 @@ To run the tests using your installed JDK, run: $ ./gradlew build ``` -We have also included a simple demo, found in the `examples/simple-console` folder. You will need a JDK installed to run it. When run, it sends several events to your event collector. +We have also included a simple demo, found in the `examples/simple-console` folder. You will need a JDK installed to run it. When run, it sends several events to your event collector. For a simple event collector, we advise using the [Snowplow Micro][micro] testing pipeline. +To run simple-console using the current Maven Central version of the Java tracker: +```bash +$ cd examples/simple-console +$ ./gradlew jar +$ java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" +``` + +To run simple-console using a local version of the Java tracker: ```bash $ ./gradlew publishToMavenLocal $ cd examples/simple-console $ ./gradlew jar $ java -jar ./build/libs/simple-console-all-0.0.1.jar "http://" ``` -For a simple event collector, we advise using the [Snowplow Micro][micro] testing pipeline. ## Copyright and license @@ -78,6 +85,7 @@ limitations under the License. [contributing-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/contributing.png [techdocs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/java-tracker/ +[apidocs]: https://snowplow.github.io/snowplow-java-tracker/index.html?overview-summary.html [tracker-classification]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/tracker-maintenance-classification/ [early-release]: https://img.shields.io/static/v1?style=flat&label=Snowplow&message=Early%20Release&color=014477&labelColor=9ba0aa&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEVMaXGXANeYANeXANZbAJmXANeUANSQAM+XANeMAMpaAJhZAJeZANiXANaXANaOAM2WANVnAKWXANZ9ALtmAKVaAJmXANZaAJlXAJZdAJxaAJlZAJdbAJlbAJmQAM+UANKZANhhAJ+EAL+BAL9oAKZnAKVjAKF1ALNBd8J1AAAAKHRSTlMAa1hWXyteBTQJIEwRgUh2JjJon21wcBgNfmc+JlOBQjwezWF2l5dXzkW3/wAAAHpJREFUeNokhQOCA1EAxTL85hi7dXv/E5YPCYBq5DeN4pcqV1XbtW/xTVMIMAZE0cBHEaZhBmIQwCFofeprPUHqjmD/+7peztd62dWQRkvrQayXkn01f/gWp2CrxfjY7rcZ5V7DEMDQgmEozFpZqLUYDsNwOqbnMLwPAJEwCopZxKttAAAAAElFTkSuQmCC diff --git a/build.gradle b/build.gradle index 3c81ed88..05cb8863 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.12.0-alpha.1' +version = '0.12.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 0ea771e8..60e6cc3d 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -18,14 +18,12 @@ import com.snowplowanalytics.snowplow.tracker.Tracker; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.events.*; -import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import java.util.List; import static java.util.Collections.singletonList; import com.google.common.collect.ImmutableMap; -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; public class Main { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index c058e2ac..105593a2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -86,8 +86,7 @@ public TrackerBuilder subject(Subject subject) { } /** - * The devicePlatform the tracker is running on ({@link DevicePlatform}). - * The default is "srv", ServerSideApp. + * The {@link DevicePlatform} the tracker is running on (default is "srv", ServerSideApp). * * @param platform The device platform the tracker is running on * @return itself @@ -98,7 +97,9 @@ public TrackerBuilder platform(DevicePlatform platform) { } /** - * @param base64 Whether JSONs in the payload should be base-64 encoded + * Whether JSONs in the payload should be base-64 encoded (default is true) + * + * @param base64 JSONs should be encoded or not * @return itself */ public TrackerBuilder base64(Boolean base64) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index cdd013da..5dea6b16 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -45,7 +45,7 @@ public static abstract class Builder> { protected abstract T self(); /** - * Set a custom ScheduledExecutorService to send http request. + * Set a custom ScheduledExecutorService to send http requests (default is ScheduledThreadPoolExecutor). *

* Implementation note: Be aware that calling `close()` on a BatchEmitter instance * has a side-effect and will shutdown that ExecutorService. @@ -59,7 +59,7 @@ public T requestExecutorService(final ScheduledExecutorService executorService) } /** - * Adds a custom HttpClientAdapter to the AbstractEmitter. The default is OkHttpClientAdapter. + * Adds a custom HttpClientAdapter to the AbstractEmitter (default is OkHttpClientAdapter). * * @param httpClientAdapter the adapter to use * @return itself @@ -70,7 +70,7 @@ public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { } /** - * Sets the Thread Count for the ScheduledExecutorService. The default is 50. + * Sets the Thread Count for the ScheduledExecutorService (default is 50). * * @param threadCount the size of the thread pool * @return itself diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 8b91a2fe..62e6380d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -200,7 +200,7 @@ public void setBatchSize(final int batchSize) { } /** - * Gets the Emitter `batchSize` + * Gets the Emitter batchSize * * @return the batch size */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 2c934d72..d77e2a28 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -78,7 +78,7 @@ public T trueTimestamp(Long timestamp) { } /** - * A custom subject for the event. Its fields will override those of the + * A custom Subject for the event. Its fields will override those of the * Tracker-associated Subject, if present. * * @param subject the eventId diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 814bc7bd..1a14af15 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -70,6 +70,8 @@ public static abstract class Builder> extends AbstractEvent private List items; /** + * Required. + * * @param orderId ID of the eCommerce transaction * @return itself */ @@ -79,6 +81,8 @@ public T orderId(String orderId) { } /** + * Required. + * * @param totalValue Total transaction value * @return itself */ @@ -88,6 +92,8 @@ public T totalValue(Double totalValue) { } /** + * Optional. + * * @param affiliation Transaction affiliation * @return itself */ @@ -97,6 +103,8 @@ public T affiliation(String affiliation) { } /** + * Optional. + * * @param taxValue Transaction tax value * @return itself */ @@ -106,6 +114,8 @@ public T taxValue(Double taxValue) { } /** + * Optional. + * * @param shipping Delivery cost charged * @return itself */ @@ -115,6 +125,8 @@ public T shipping(Double shipping) { } /** + * Optional. + * * @param city Delivery address city * @return itself */ @@ -124,6 +136,8 @@ public T city(String city) { } /** + * Optional. + * * @param state Delivery address state * @return itself */ @@ -133,6 +147,8 @@ public T state(String state) { } /** + * Optional. + * * @param country Delivery address country * @return itself */ @@ -142,6 +158,8 @@ public T country(String country) { } /** + * Optional. + * * @param currency The currency the price is expressed in * @return itself */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index c601df7b..aa4b4ab5 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -59,6 +59,8 @@ public static abstract class Builder> extends AbstractEvent private String currency; /** + * Required. + * * @param itemId Item ID - ideally the same as the EcommerceTransaction orderId * @return itself */ @@ -68,6 +70,8 @@ public T itemId(String itemId) { } /** + * Required. + * * @param sku Item SKU * @return itself */ @@ -77,6 +81,8 @@ public T sku(String sku) { } /** + * Required. + * * @param price Item price * @return itself */ @@ -86,6 +92,8 @@ public T price(Double price) { } /** + * Required. + * * @param quantity Item quantity * @return itself */ @@ -95,6 +103,8 @@ public T quantity(Integer quantity) { } /** + * Optional. + * * @param name Item name * @return itself */ @@ -104,6 +114,8 @@ public T name(String name) { } /** + * Optional. + * * @param category Item category * @return itself */ @@ -113,6 +125,8 @@ public T category(String category) { } /** + * Optional. + * * @param currency The currency the price is expressed in * @return itself */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index 8c9c7df7..da9becbb 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -38,6 +38,8 @@ public static abstract class Builder> extends AbstractEvent private String referrer; /** + * Required. + * * @param pageUrl URL of the viewed page * @return itself */ @@ -47,6 +49,8 @@ public T pageUrl(String pageUrl) { } /** + * Optional. + * * @param pageTitle Title of the viewed page * @return itself */ @@ -56,6 +60,8 @@ public T pageTitle(String pageTitle) { } /** + * Optional. + * * @param referrer Referrer URL of the page * @return itself */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 91cbd2c6..7fe2691b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -36,6 +36,8 @@ public static abstract class Builder> extends AbstractEvent private String id; /** + * One of name or id is required. + * * @param name The (human-readable) name of the screen view * @return itself */ @@ -45,6 +47,8 @@ public T name(String name) { } /** + * One of name or id is required. + * * @param id Screen view ID * @return itself */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index 6e0f0d6f..95f7fb43 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -50,6 +50,8 @@ public static abstract class Builder> extends AbstractEvent private Double value; /** + * Required. + * * @param category Category of the event * @return itself */ @@ -59,6 +61,8 @@ public T category(String category) { } /** + * Required. + * * @param action Describes what happened in the event * @return itself */ @@ -68,6 +72,8 @@ public T action(String action) { } /** + * Optional. + * * @param label Refers to the object the action is performed on * @return itself */ @@ -77,6 +83,8 @@ public T label(String label) { } /** + * Optional. + * * @param property Property associated with either the action or the object * @return itself */ @@ -86,6 +94,8 @@ public T property(String property) { } /** + * Optional. + * * @param value A value associated with the user action * @return itself */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index fdcdf4b2..f61370dc 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -43,6 +43,8 @@ public static abstract class Builder> extends AbstractEvent private String label; /** + * Required. + * * @param category The category of the timed event * @return itself */ @@ -52,6 +54,8 @@ public T category(String category) { } /** + * Required. + * * @param variable Identify the timing being recorded * @return itself */ @@ -61,6 +65,8 @@ public T variable(String variable) { } /** + * Required. + * * @param timing The number of milliseconds in elapsed time to report * @return itself */ @@ -70,6 +76,8 @@ public T timing(Integer timing) { } /** + * Optional. + * * @param label Optional description of this timing * @return itself */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java index 25534349..31322215 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java @@ -39,6 +39,8 @@ public static abstract class Builder> extends AbstractEvent private SelfDescribingJson eventData; /** + * Required. + * * @param selfDescribingJson The properties of the event. Has two fields: "data", containing the event properties, * and "schema", identifying the schema against which the data is validated * @return itself diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index f886ba1a..155228dc 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -35,7 +35,7 @@ public class SelfDescribingJson implements Payload { /** * Creates a SelfDescribingJson with only a Schema - * String and an empty data map. Data can be added later using {@link #setData(Object)}. + * String and an empty data map. Data can be added later using setData() methods. * * @param schema the schema string */ diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 2d234fb7..3ddfd1ae 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -580,7 +580,7 @@ public void testTrackTimingWithSubject() throws InterruptedException { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); - assertEquals("java-0.12.0-alpha.1", tracker.getTrackerVersion()); + assertEquals("java-0.12.0", tracker.getTrackerVersion()); } @Test From 6f2d537cc69d0c25ef68c1c4fb3101e4771a4cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:14:54 +0200 Subject: [PATCH 084/128] Bump junit to 4.13.2 (close #330) --- examples/simple-console/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 97217503..e5dbb6bf 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -27,7 +27,7 @@ dependencies { implementation 'org.slf4j:slf4j-simple:1.7.30' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' - testCompileOnly 'junit:junit:4.13' + testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } From ec1d4e5b2413d13f5e0a6d9c94354a3247cdc2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:15:22 +0200 Subject: [PATCH 085/128] Bump mockwebserver to 4.9.3 (close #329) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 05cb8863..42687fd9 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,7 @@ dependencies { testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.3' } task sourceJar(type: Jar, dependsOn: 'generateSources') { From 04714b4c2a072850e965100da0ba765a43b03e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:15:56 +0200 Subject: [PATCH 086/128] Bump junit-jupiter-api to 5.8.2 (close #328) --- build.gradle | 2 +- examples/simple-console/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 42687fd9..ea1b80ab 100644 --- a/build.gradle +++ b/build.gradle @@ -79,7 +79,7 @@ dependencies { api 'com.google.guava:guava:31.0-jre' // Testing libraries - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index e5dbb6bf..47e2e6e6 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -26,7 +26,7 @@ dependencies { implementation 'org.slf4j:slf4j-simple:1.7.30' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } From fb340f32c4f8df891393c4373a2767017a74d619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:16:22 +0200 Subject: [PATCH 087/128] Bump guava to 31.1-jre (close #327) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ea1b80ab..df92fe47 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ dependencies { api 'com.fasterxml.jackson.core:jackson-databind:2.13.1' // Preconditions - api 'com.google.guava:guava:31.0-jre' + api 'com.google.guava:guava:31.1-jre' // Testing libraries testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' From c6e50fecddf5b640c5875b7810781bb78238b80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:16:38 +0200 Subject: [PATCH 088/128] Bump jackson-databind to 2.13.2.2 (close #326) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df92fe47..9a0d862b 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ dependencies { testImplementation 'org.slf4j:slf4j-simple:1.7.30' // Jackson JSON processor - api 'com.fasterxml.jackson.core:jackson-databind:2.13.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' // Preconditions api 'com.google.guava:guava:31.1-jre' From db006545d8cca158eebf4c75f0d4b360c86479c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:17:14 +0200 Subject: [PATCH 089/128] Bump slf4j-simple to 1.7.36 (close #325) --- build.gradle | 2 +- examples/simple-console/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9a0d862b..ce50d6ca 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ dependencies { // SLF4J logging API api 'org.slf4j:slf4j-api:1.7.30' - testImplementation 'org.slf4j:slf4j-simple:1.7.30' + testImplementation 'org.slf4j:slf4j-simple:1.7.36' // Jackson JSON processor api 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 47e2e6e6..b021e09b 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -24,7 +24,7 @@ dependencies { } } - implementation 'org.slf4j:slf4j-simple:1.7.30' + implementation 'org.slf4j:slf4j-simple:1.7.36' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testCompileOnly 'junit:junit:4.13.2' From 5e7f4cb47a0745a8bd34c9739a1318ddc41b3ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:17:27 +0200 Subject: [PATCH 090/128] Bump slf4j-api to 1.7.36 (close #324) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ce50d6ca..4c133f28 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ dependencies { okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.2.2' // SLF4J logging API - api 'org.slf4j:slf4j-api:1.7.30' + api 'org.slf4j:slf4j-api:1.7.36' testImplementation 'org.slf4j:slf4j-simple:1.7.36' // Jackson JSON processor From 01c923f03831c1b3d0fd0afd5ab24e78dc33c6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:17:43 +0200 Subject: [PATCH 091/128] Bump okhttp to 4.9.3 (close #323) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c133f28..2ec1f347 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.4' // Square OK HTTP - okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.2.2' + okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.9.3' // SLF4J logging API api 'org.slf4j:slf4j-api:1.7.36' From 83344b1486c5dd39232d34a3fb48a353ad9db57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:17:55 +0200 Subject: [PATCH 092/128] Bump httpasyncclient to 4.1.5 (close #322) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2ec1f347..67dcdf53 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ dependencies { // Apache HTTP apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.13' - apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.4' + apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.5' // Square OK HTTP okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.9.3' From 724b4d3c119a369da4302ab72cecaafe30fab97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 09:18:09 +0200 Subject: [PATCH 093/128] Bump commons-codec to 1.15 (close #321) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 67dcdf53..a7e63e99 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ test { dependencies { // Apache Commons - api 'commons-codec:commons-codec:1.14' + api 'commons-codec:commons-codec:1.15' api 'commons-net:commons-net:3.6' // Apache HTTP From 54f061e8016473525a36e3ced50f01b8ad517fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 11 May 2022 10:26:03 +0200 Subject: [PATCH 094/128] Prepare for 0.12.1 release --- CHANGELOG | 13 +++++++++++++ build.gradle | 2 +- .../snowplow/tracker/TrackerTest.java | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e55776f2..c54ca257 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,16 @@ +Java 0.12.1 (2022-05-11) +----------------------- +Bump junit to 4.13.2 (#330) +Bump mockwebserver to 4.9.3 (#329) +Bump junit-jupiter-api to 5.8.2 (#328) +Bump guava to 31.1-jre (#327) +Bump jackson-databind to 2.13.2.2 (#326) +Bump slf4j-simple to 1.7.36 (#325) +Bump slf4j-api to 1.7.36 (#324) +Bump okhttp to 4.9.3 (#323) +Bump httpasyncclient to 4.1.5 (#322) +Bump commons-codec to 1.15 (#321) + Java 0.12.0 (2022-03-24) ----------------------- Choose HTTP response codes not to retry (#316) diff --git a/build.gradle b/build.gradle index a7e63e99..68461a32 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.12.0' +version = '0.12.1' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 3ddfd1ae..0c22306b 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -580,7 +580,7 @@ public void testTrackTimingWithSubject() throws InterruptedException { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); - assertEquals("java-0.12.0", tracker.getTrackerVersion()); + assertEquals("java-0.12.1", tracker.getTrackerVersion()); } @Test From 8e5b884bc566e32c526deecb14559194dd639f50 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Fri, 17 Jun 2022 14:35:55 +0100 Subject: [PATCH 095/128] Bump jackson-databind to 2.13.3 (close #333) PR #334 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 68461a32..ed2ae890 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ dependencies { testImplementation 'org.slf4j:slf4j-simple:1.7.36' // Jackson JSON processor - api 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.3' // Preconditions api 'com.google.guava:guava:31.1-jre' From 20f63f83e6c925b042769f3113aceb5e4657021f Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Fri, 17 Jun 2022 14:40:34 +0100 Subject: [PATCH 096/128] Prepare for 0.12.2 release --- CHANGELOG | 4 ++++ build.gradle | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c54ca257..98b76064 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +Java 0.12.2 (2022-06-17) +----------------------- +Bump jackson-databind to 2.13.3 (#333) + Java 0.12.1 (2022-05-11) ----------------------- Bump junit to 4.13.2 (#330) diff --git a/build.gradle b/build.gradle index ed2ae890..60f7ca9d 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.12.1' +version = '0.12.2' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 0c22306b..eaf016d8 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -580,7 +580,7 @@ public void testTrackTimingWithSubject() throws InterruptedException { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); - assertEquals("java-0.12.1", tracker.getTrackerVersion()); + assertEquals("java-0.12.2", tracker.getTrackerVersion()); } @Test From ae8f8ebb399d6d403c6bd6c1c66ac9fe7c5815fc Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 27 Jul 2022 14:48:11 +0100 Subject: [PATCH 097/128] Rename Unstructured events to SelfDescribing (close #296) For PR #344 * Rename Unstructured events to SelfDescribing (close #296) * Remove last Unstructured mentions --- .../main/java/com/snowplowanalytics/Main.java | 12 +++++------ .../snowplow/tracker/Tracker.java | 20 +++++++++---------- .../snowplow/tracker/constants/Constants.java | 4 ++-- .../snowplow/tracker/constants/Parameter.java | 4 ++-- .../tracker/events/EcommerceTransaction.java | 2 +- .../events/EcommerceTransactionItem.java | 2 +- .../snowplow/tracker/events/ScreenView.java | 4 ++-- ...{Unstructured.java => SelfDescribing.java} | 18 ++++++++--------- .../snowplow/tracker/events/Structured.java | 2 +- .../snowplow/tracker/events/Timing.java | 4 ++-- .../snowplow/tracker/TrackerTest.java | 18 ++++++++--------- 11 files changed, 44 insertions(+), 46 deletions(-) rename src/main/java/com/snowplowanalytics/snowplow/tracker/events/{Unstructured.java => SelfDescribing.java} (84%) diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 60e6cc3d..28772477 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -107,10 +107,8 @@ public static void main(String[] args) throws InterruptedException { .build(); - // This is an example of a custom "Unstructured" event based on a schema - // Unstructured events are also called "self-describing" events - // because of their SelfDescribingJson base - Unstructured unstructured = Unstructured.builder() + // This is an example of a custom SelfDescribing event based on a schema + SelfDescribing selfDescribing = SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.iglu/anything-a/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") @@ -119,7 +117,7 @@ public static void main(String[] args) throws InterruptedException { .build(); - // This is an example of a ScreenView event which will be translated into an Unstructured event + // This is an example of a ScreenView event which will be translated into a SelfDescribing event ScreenView screenView = ScreenView.builder() .name("name") .id("id") @@ -127,7 +125,7 @@ public static void main(String[] args) throws InterruptedException { .build(); - // This is an example of a Timing event which will be translated into an Unstructured event + // This is an example of a Timing event which will be translated into a SelfDescribing event Timing timing = Timing.builder() .category("category") .label("label") @@ -148,7 +146,7 @@ public static void main(String[] args) throws InterruptedException { tracker.track(pageViewEvent); // the .track method schedules the event for delivery to Snowplow tracker.track(ecommerceTransaction); // This will track two events - tracker.track(unstructured); + tracker.track(selfDescribing); tracker.track(screenView); tracker.track(timing); tracker.track(structured); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 105593a2..c9d69872 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -248,11 +248,11 @@ private List eventTypeSpecificPreProcessing(Event event) { List eventList = new ArrayList<>(); final Class eventClass = event.getClass(); - if (eventClass.equals(Unstructured.class)) { - // Need to set the Base64 rule for Unstructured events - final Unstructured unstructured = (Unstructured) event; - unstructured.setBase64Encode(parameters.getBase64Encoded()); - eventList.add(unstructured); + if (eventClass.equals(SelfDescribing.class)) { + // Need to set the Base64 rule for SelfDescribing events + final SelfDescribing selfDescribing = (SelfDescribing) event; + selfDescribing.setBase64Encode(parameters.getBase64Encoded()); + eventList.add(selfDescribing); } else if (eventClass.equals(EcommerceTransaction.class)) { final EcommerceTransaction ecommerceTransaction = (EcommerceTransaction) event; @@ -262,17 +262,17 @@ private List eventTypeSpecificPreProcessing(Event event) { eventList.addAll(ecommerceTransaction.getItems()); } else if (eventClass.equals(Timing.class) || eventClass.equals(ScreenView.class)) { - // Timing and ScreenView events are wrapper classes for Unstructured events - // Need to create Unstructured events from them to send. - final Unstructured unstructured = Unstructured.builder() + // Timing and ScreenView events are wrapper classes for SelfDescribing events + // Need to create SelfDescribing events from them to send. + final SelfDescribing selfDescribing = SelfDescribing.builder() .eventData((SelfDescribingJson) event.getPayload()) .customContext(event.getContext()) .trueTimestamp(event.getTrueTimestamp()) .subject(event.getSubject()) .build(); - unstructured.setBase64Encode(parameters.getBase64Encoded()); - eventList.add(unstructured); + selfDescribing.setBase64Encode(parameters.getBase64Encoded()); + eventList.add(selfDescribing); } else { eventList.add(event); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java index 514dafc1..52443033 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Constants.java @@ -22,7 +22,7 @@ public class Constants { public static final String SCHEMA_PAYLOAD_DATA = "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4"; public static final String SCHEMA_CONTEXTS = "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1"; - public static final String SCHEMA_UNSTRUCT_EVENT = "iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0"; + public static final String SCHEMA_SELF_DESCRIBING_EVENT = "iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0"; public static final String SCHEMA_SCREEN_VIEW = "iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0"; public static final String SCHEMA_USER_TIMINGS = "iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0"; @@ -30,7 +30,7 @@ public class Constants { public static final String EVENT_PAGE_VIEW = "pv"; public static final String EVENT_STRUCTURED = "se"; - public static final String EVENT_UNSTRUCTURED = "ue"; + public static final String EVENT_SELF_DESCRIBING = "ue"; public static final String EVENT_ECOMM = "tr"; public static final String EVENT_ECOMM_ITEM = "ti"; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index abc01458..9cb71336 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -39,8 +39,8 @@ public class Parameter { public static final String UID = "uid"; public static final String CONTEXT = "co"; public static final String CONTEXT_ENCODED = "cx"; - public static final String UNSTRUCTURED = "ue_pr"; - public static final String UNSTRUCTURED_ENCODED = "ue_px"; + public static final String SELF_DESCRIBING = "ue_pr"; + public static final String SELF_DESCRIBING_ENCODED = "ue_px"; // Subject class public static final String PLATFORM = "p"; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 1a14af15..399f3847 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -29,7 +29,7 @@ * Constructs an EcommerceTransaction event object. *

* Implementation note: EcommerceTransaction/EcommerceTransactionItem uses a legacy design. - * We aim to deprecate it eventually. We advise using Unstructured events instead, and attaching the items + * We aim to deprecate it eventually. We advise using SelfDescribing events instead, and attaching the items * as entities. * * The specific items purchased in the transaction must be added as EcommerceTransactionItem objects. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index aa4b4ab5..9b12d34b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -25,7 +25,7 @@ * Constructs an EcommerceTransactionItem object. *

* Implementation note: EcommerceTransaction/EcommerceTransactionItem uses a legacy design. - * We aim to deprecate it eventually. We advise using Unstructured events instead, and attaching the items + * We aim to deprecate it eventually. We advise using SelfDescribing events instead, and attaching the items * as entities. * * EcommerceTransactionItems were designed for attaching data about purchased items to a diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 7fe2691b..3a42020b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -23,7 +23,7 @@ /** * Constructs a ScreenView event object. * - * When tracked, generates an "unstructured" or "ue" event. + * When tracked, generates a SelfDescribing event (event type "ue"). */ public class ScreenView extends AbstractEvent { @@ -85,7 +85,7 @@ protected ScreenView(Builder builder) { /** * Return the payload wrapped into a SelfDescribingJson. When a ScreenView is tracked, - * the Tracker creates and tracks an Unstructured event from this SelfDescribingJson. + * the Tracker creates and tracks an SelfDescribing event from this SelfDescribingJson. * * @return the payload as a SelfDescribingJson. */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java similarity index 84% rename from src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java rename to src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java index 31322215..e9732e9f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Unstructured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java @@ -22,14 +22,14 @@ import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; /** - * Constructs an Unstructured event object. + * Constructs a SelfDescribing event object. * * This is a customisable event type which allows you to track anything describable * by a JsonSchema. * - * When tracked, generates an "unstructured" or "ue" event. + * When tracked, generates a self-describing event (event type "ue"). */ -public class Unstructured extends AbstractEvent { +public class SelfDescribing extends AbstractEvent { private final SelfDescribingJson eventData; private boolean base64Encode; @@ -50,8 +50,8 @@ public T eventData(SelfDescribingJson selfDescribingJson) { return self(); } - public Unstructured build() { - return new Unstructured(this); + public SelfDescribing build() { + return new SelfDescribing(this); } } @@ -66,7 +66,7 @@ public static Builder builder() { return new Builder2(); } - protected Unstructured(Builder builder) { + protected SelfDescribing(Builder builder) { super(builder); // Precondition checks @@ -90,10 +90,10 @@ public void setBase64Encode(boolean base64Encode) { public TrackerPayload getPayload() { TrackerPayload payload = new TrackerPayload(); SelfDescribingJson envelope = new SelfDescribingJson( - Constants.SCHEMA_UNSTRUCT_EVENT, this.eventData.getMap()); - payload.add(Parameter.EVENT, Constants.EVENT_UNSTRUCTURED); + Constants.SCHEMA_SELF_DESCRIBING_EVENT, this.eventData.getMap()); + payload.add(Parameter.EVENT, Constants.EVENT_SELF_DESCRIBING); payload.addMap(envelope.getMap(), this.base64Encode, - Parameter.UNSTRUCTURED_ENCODED, Parameter.UNSTRUCTURED); + Parameter.SELF_DESCRIBING_ENCODED, Parameter.SELF_DESCRIBING); return putTrueTimestamp(payload); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index 95f7fb43..d4361b65 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -28,7 +28,7 @@ * To aid data quality and modeling, agree on business-wide definitions when designing * your tracking strategy. * - * We recommend using Unstructured - fully custom - events instead. + * We recommend using SelfDescribing - fully custom - events instead. * * When tracked, generates a "struct" or "se" event. * diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index f61370dc..7bed6f3e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -26,7 +26,7 @@ /** * Constructs a Timing event object. * - * When tracked, generates an "unstructured" or "ue" event. + * When tracked, generates a SelfDescribing event (event type "ue"). */ public class Timing extends AbstractEvent { @@ -120,7 +120,7 @@ protected Timing(Builder builder) { /** * Return the payload wrapped into a SelfDescribingJson. When a Timing event is tracked, - * the Tracker creates and tracks an Unstructured event from this SelfDescribingJson. + * the Tracker creates and tracks a SelfDescribing event from this SelfDescribingJson. * * @return the payload as a SelfDescribingJson. */ diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index eaf016d8..0a9acc1a 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -68,7 +68,7 @@ public void setUp() { @Test public void testTrackReturnsEventIdIfSuccessful() throws InterruptedException { // a list to allow for eCommerceTransaction - List result = tracker.track(Unstructured.builder() + List result = tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") @@ -104,7 +104,7 @@ public void flushBuffer() {} FailingMockEmitter failingMockEmitter = new FailingMockEmitter(); tracker = new Tracker.TrackerBuilder(failingMockEmitter, "AF003", "cloudfront").build(); - List result = tracker.track(Unstructured.builder() + List result = tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") @@ -241,9 +241,9 @@ public void testEcommerceTransactionItemAlone() throws InterruptedException { } @Test - public void testUnstructuredEventWithContext() throws InterruptedException { + public void testSelfDescribingEventWithContext() throws InterruptedException { // When - tracker.track(Unstructured.builder() + tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") @@ -272,9 +272,9 @@ public void testUnstructuredEventWithContext() throws InterruptedException { } @Test - public void testUnstructuredEventWithoutContext() throws InterruptedException { + public void testSelfDescribingEventWithoutContext() throws InterruptedException { // When - tracker.track(Unstructured.builder() + tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "baær") @@ -301,9 +301,9 @@ public void testUnstructuredEventWithoutContext() throws InterruptedException { } @Test - public void testUnstructuredEventWithoutTrueTimestamp() throws InterruptedException { + public void testSelfDescribingEventWithoutTrueTimestamp() throws InterruptedException { // When - tracker.track(Unstructured.builder() + tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") @@ -375,7 +375,7 @@ public void testTrackTwoEvents() throws InterruptedException { .trueTimestamp(123456L) .build()); - tracker.track(Unstructured.builder() + tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", ImmutableMap.of("foo", "bar") From e0f4d4425f5aa4174f8ac774c8ed5b1bf67aa375 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 27 Jul 2022 14:50:02 +0100 Subject: [PATCH 098/128] Add support for storing cookies in OkHttpClientAdapter (close #336) For PR #342 * Start implementing CookieJar * Store cookies in static set * Save response cookies and add them to future requests * Add copyright notices * Tidy up cookie jar tests * Don't save cookies by default --- .../snowplow/tracker/Subject.java | 4 +- .../tracker/emitter/AbstractEmitter.java | 36 ++++- .../tracker/http/CollectorCookie.java | 59 ++++++++ .../tracker/http/CollectorCookieJar.java | 67 +++++++++ .../tracker/http/OkHttpClientAdapter.java | 10 +- .../emitter/CollectorCookieJarTest.java | 128 ++++++++++++++++++ .../tracker/http/HttpClientAdapterTest.java | 31 +++++ 7 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java create mode 100644 src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index d058c847..fb803f9a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -309,8 +309,8 @@ public void setDomainSessionId(String domainSessionId) { } /** - * User inputted Network User Id for the - * subject. + * User inputted Network User ID for the subject. + * This overrides the network user ID set by the Collector in response Cookies. * * @param networkUserId a network user id */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java index 5dea6b16..860302b0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java @@ -17,13 +17,16 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collector; import com.google.common.base.Preconditions; +import com.snowplowanalytics.snowplow.tracker.http.CollectorCookieJar; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import okhttp3.CookieJar; import okhttp3.OkHttpClient; /** @@ -42,6 +45,7 @@ public static abstract class Builder> { private int threadCount = 50; // Optional private ScheduledExecutorService requestExecutorService = null; // Optional private String collectorUrl = null; // Required if not specifying a httpClientAdapter + private CookieJar cookieJar = null; // Optional protected abstract T self(); /** @@ -91,6 +95,18 @@ public T url(final String collectorUrl) { this.collectorUrl = collectorUrl; return self(); } + + /** + * Adds a custom CookieJar to be used with OkHttpClientAdapters. + * Will be ignored if a custom httpClientAdapter is provided. + * + * @param cookieJar the CookieJar to use + * @return itself + */ + public T cookieJar(final CookieJar cookieJar) { + this.cookieJar = cookieJar; + return self(); + } } private static class Builder2 extends Builder { @@ -105,26 +121,34 @@ public static Builder builder() { } protected AbstractEmitter(final Builder builder) { + OkHttpClient client; // Precondition checks Preconditions.checkArgument(builder.threadCount > 0, "threadCount must be greater than 0"); if (builder.httpClientAdapter != null) { - this.httpClientAdapter = builder.httpClientAdapter; + httpClientAdapter = builder.httpClientAdapter; } else { Preconditions.checkNotNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); - this.httpClientAdapter = OkHttpClientAdapter.builder() + if (builder.cookieJar != null) { + client = new OkHttpClient.Builder() + .cookieJar(builder.cookieJar) + .build(); + } else { + client = new OkHttpClient.Builder().build(); + } + + httpClientAdapter = OkHttpClientAdapter.builder() // use okhttp as a default .url(builder.collectorUrl) - .httpClient( - new OkHttpClient()) // use okhttp as a default + .httpClient(client) .build(); } if (builder.requestExecutorService != null) { - this.executor = builder.requestExecutorService; + executor = builder.requestExecutorService; } else { - this.executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); + executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java new file mode 100644 index 00000000..d300ccbb --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.http; + +import okhttp3.Cookie; + +import java.util.*; + +public class CollectorCookie { + private final Cookie cookie; + + static List decorateAll(Collection cookies) { + List collectorCookies = new ArrayList<>(cookies.size()); + for (Cookie cookie : cookies) { + collectorCookies.add(new CollectorCookie(cookie)); + } + return collectorCookies; + } + + CollectorCookie(Cookie cookie) { + this.cookie = cookie; + } + + public boolean isExpired() { + return cookie.expiresAt() < System.currentTimeMillis(); + } + + Cookie getCookie() { + return cookie; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof CollectorCookie)) return false; + CollectorCookie that = (CollectorCookie) other; + return that.cookie.name().equals(this.cookie.name()) + && that.cookie.domain().equals(this.cookie.domain()) + && that.cookie.path().equals(this.cookie.path()); + } + + @Override + public int hashCode() { + int hash = 17; + hash = 31 * hash + cookie.name().hashCode(); + hash = 31 * hash + cookie.domain().hashCode(); + hash = 31 * hash + cookie.path().hashCode(); + return hash; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java new file mode 100644 index 00000000..ac1a683a --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.http; + +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class CollectorCookieJar implements okhttp3.CookieJar { + private static final Set cookies = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + @Override + public List loadForRequest(HttpUrl url) { + List cookiesToRemove = new ArrayList<>(); + List validCookies = new ArrayList<>(); + + for (CollectorCookie currentCookie : cookies) { + if (currentCookie.isExpired()) { + cookiesToRemove.add(currentCookie); + } else if (currentCookie.getCookie().matches(url)) { + validCookies.add(currentCookie.getCookie()); + } + } + + if (!cookiesToRemove.isEmpty()) { + removeAll(cookiesToRemove); + } + + return validCookies; + } + + @Override + public void saveFromResponse(HttpUrl httpUrl, List cookies) { + saveAll(cookies); + } + + public void clear() { + cookies.clear(); + } + + private void saveAll(Collection newCookies) { + for (CollectorCookie cookie : CollectorCookie.decorateAll(newCookies)) { + cookies.remove(cookie); + cookies.add(cookie); + } + + } + + private void removeAll(Collection cookiesToRemove) { + for (CollectorCookie cookie : cookiesToRemove) { + cookies.remove(cookie); + } + } +} + diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 98dc6026..b476fa71 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -19,11 +19,7 @@ import com.google.common.base.Preconditions; // SquareUp -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.MediaType; -import okhttp3.Response; -import okhttp3.RequestBody; +import okhttp3.*; // Slf4j import org.slf4j.Logger; @@ -47,7 +43,7 @@ public static abstract class Builder> extends AbstractHttpC private OkHttpClient httpClient; // Required /** - * @param httpClient The Apache HTTP Client to use + * @param httpClient The OkHTTP Client to use * @return itself */ public T httpClient(OkHttpClient httpClient) { @@ -77,7 +73,7 @@ protected OkHttpClientAdapter(Builder builder) { // Precondition checks Preconditions.checkNotNull(builder.httpClient); - this.httpClient = builder.httpClient; + httpClient = builder.httpClient; } /** diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java new file mode 100644 index 00000000..6959b280 --- /dev/null +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import com.snowplowanalytics.snowplow.tracker.http.CollectorCookieJar; +import okhttp3.Cookie; +import okhttp3.HttpUrl; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class CollectorCookieJarTest { + String domain1 = "http://snowplow.test.url.com"; + String domain2 = "http://other.test.url.com"; + Cookie cookie1; + CollectorCookieJar cookieJar; + List requestCookies; + + @Before + public void setUp() { + cookie1 = new Cookie.Builder() + .name("sp") + .value("xxx") + .domain("snowplow.test.url.com") + .build(); + cookieJar = new CollectorCookieJar(); + } + + @Test + public void testNoCookiesAtStartup() { + List cookies = cookieJar.loadForRequest(HttpUrl.parse(domain1)); + assertTrue(cookies.isEmpty()); + } + + @Test + public void testReturnsCookiesAfterSetInResponse() { + requestCookies = Collections.singletonList(cookie1); + cookieJar.saveFromResponse( + HttpUrl.parse(domain1), + requestCookies + ); + + List cookies2 = cookieJar.loadForRequest(HttpUrl.parse(domain1)); + assertFalse(cookies2.isEmpty()); + assertEquals(cookies2.get(0).name(), "sp"); + + cookieJar.clear(); + } + + @Test + public void testDoesntReturnCookiesForDifferentDomain() { + requestCookies = Collections.singletonList(cookie1); + cookieJar.saveFromResponse( + HttpUrl.parse(domain1), + requestCookies + ); + + List cookies2 = cookieJar.loadForRequest(HttpUrl.parse(domain2)); + assertTrue(cookies2.isEmpty()); + + cookieJar.clear(); + } + + @Test + public void testMaintainsCookiesAcrossJarInstances() { + requestCookies = Collections.singletonList(cookie1); + cookieJar.saveFromResponse( + HttpUrl.parse(domain1), + requestCookies + ); + + CollectorCookieJar cookieJar2 = new CollectorCookieJar(); + List cookies2 = cookieJar2.loadForRequest(HttpUrl.parse(domain1)); + assertFalse(cookies2.isEmpty()); + + cookieJar.clear(); + } + + @Test + public void testClearsCookies() { + requestCookies = Collections.singletonList(cookie1); + cookieJar.saveFromResponse( + HttpUrl.parse(domain1), + requestCookies + ); + + List cookies = cookieJar.loadForRequest(HttpUrl.parse(domain1)); + assertFalse(cookies.isEmpty()); + + cookieJar.clear(); + List cookies2 = cookieJar.loadForRequest(HttpUrl.parse(domain1)); + assertTrue(cookies2.isEmpty()); + } + + @Test + public void testRemovesExpiredCookies() { + Cookie cookie2 = new Cookie.Builder() + .name("sp") + .value("xxx") + .domain("snowplow.test.url.com") + .expiresAt(1654869235L) + .build(); + + requestCookies = Collections.singletonList(cookie2); + cookieJar.saveFromResponse( + HttpUrl.parse(domain1), + requestCookies + ); + + List cookies = cookieJar.loadForRequest(HttpUrl.parse(domain1)); + assertTrue(cookies.isEmpty()); + } + +} diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 7e61050c..3d7ade9f 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -15,10 +15,12 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.EnumSet; import java.util.concurrent.TimeUnit; import com.google.common.collect.ImmutableMap; +import com.snowplowanalytics.snowplow.tracker.payload.Payload; import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -130,4 +132,33 @@ public void testPostWithNullArgument() { public void testGetWithNullArgument() { Assert.assertThrows(NullPointerException.class, () -> adapter.get(null)); } + + @Test + public void testRequestWithCookies() throws IOException, InterruptedException { + OkHttpClient httpClient = new OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(1, TimeUnit.SECONDS) + .writeTimeout(1, TimeUnit.SECONDS) + .cookieJar(new CollectorCookieJar()) + .build(); + adapter = OkHttpClientAdapter.builder() + .url(mockWebServer.url("/").toString()) + .httpClient(httpClient) + .build(); + + mockWebServer.enqueue(new MockResponse().addHeader("Set-Cookie", "sp=test")); + + SelfDescribingJson payload = new SelfDescribingJson("schema", ImmutableMap.of("foo", "bar")); + + adapter.post(payload); + adapter.post(payload); + + assertEquals(2, mockWebServer.getRequestCount()); + mockWebServer.takeRequest(); + RecordedRequest recordedRequest2 = mockWebServer.takeRequest(); + + assertEquals("sp=test", recordedRequest2.getHeader("Cookie")); + + mockWebServer.shutdown(); + } } From 8d23b3aea170b796f25d83db0868ab4c28253b02 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 27 Jul 2022 14:57:47 +0100 Subject: [PATCH 099/128] Remove SimpleEmitter (close #341) For PR #343 Restore missing CookieJar code --- .../tracker/emitter/AbstractEmitter.java | 233 ------------------ .../tracker/emitter/BatchEmitter.java | 182 ++++++++++++-- .../snowplow/tracker/emitter/Emitter.java | 2 - .../tracker/emitter/SimpleEmitter.java | 136 ---------- 4 files changed, 163 insertions(+), 390 deletions(-) delete mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java delete mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java deleted file mode 100644 index 860302b0..00000000 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/AbstractEmitter.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package com.snowplowanalytics.snowplow.tracker.emitter; - -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collector; - -import com.google.common.base.Preconditions; - -import com.snowplowanalytics.snowplow.tracker.http.CollectorCookieJar; -import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; -import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; - -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import okhttp3.CookieJar; -import okhttp3.OkHttpClient; - -/** - * AbstractEmitter class which contains common elements to - * the emitters wrapped in a builder format. - * Note that SimpleEmitter has been deprecated. - */ -public abstract class AbstractEmitter implements Emitter { - - protected HttpClientAdapter httpClientAdapter; - protected ScheduledExecutorService executor; - - public static abstract class Builder> { - - private HttpClientAdapter httpClientAdapter; // Optional - private int threadCount = 50; // Optional - private ScheduledExecutorService requestExecutorService = null; // Optional - private String collectorUrl = null; // Required if not specifying a httpClientAdapter - private CookieJar cookieJar = null; // Optional - protected abstract T self(); - - /** - * Set a custom ScheduledExecutorService to send http requests (default is ScheduledThreadPoolExecutor). - *

- * Implementation note: Be aware that calling `close()` on a BatchEmitter instance - * has a side-effect and will shutdown that ExecutorService. - * - * @param executorService the ScheduledExecutorService to use - * @return itself - */ - public T requestExecutorService(final ScheduledExecutorService executorService) { - this.requestExecutorService = executorService; - return self(); - } - - /** - * Adds a custom HttpClientAdapter to the AbstractEmitter (default is OkHttpClientAdapter). - * - * @param httpClientAdapter the adapter to use - * @return itself - */ - public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { - this.httpClientAdapter = httpClientAdapter; - return self(); - } - - /** - * Sets the Thread Count for the ScheduledExecutorService (default is 50). - * - * @param threadCount the size of the thread pool - * @return itself - */ - public T threadCount(final int threadCount) { - this.threadCount = threadCount; - return self(); - } - - /** - * Sets the emitter url for when a httpClientAdapter is not specified. - * It will be used to create the default OkHttpClientAdapter. - * - * @param collectorUrl the url for the default httpClientAdapter - * @return itself - */ - public T url(final String collectorUrl) { - this.collectorUrl = collectorUrl; - return self(); - } - - /** - * Adds a custom CookieJar to be used with OkHttpClientAdapters. - * Will be ignored if a custom httpClientAdapter is provided. - * - * @param cookieJar the CookieJar to use - * @return itself - */ - public T cookieJar(final CookieJar cookieJar) { - this.cookieJar = cookieJar; - return self(); - } - } - - private static class Builder2 extends Builder { - @Override - protected Builder2 self() { - return this; - } - } - - public static Builder builder() { - return new Builder2(); - } - - protected AbstractEmitter(final Builder builder) { - OkHttpClient client; - - // Precondition checks - Preconditions.checkArgument(builder.threadCount > 0, "threadCount must be greater than 0"); - - if (builder.httpClientAdapter != null) { - httpClientAdapter = builder.httpClientAdapter; - } else { - Preconditions.checkNotNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); - - if (builder.cookieJar != null) { - client = new OkHttpClient.Builder() - .cookieJar(builder.cookieJar) - .build(); - } else { - client = new OkHttpClient.Builder().build(); - } - - httpClientAdapter = OkHttpClientAdapter.builder() // use okhttp as a default - .url(builder.collectorUrl) - .httpClient(client) - .build(); - } - - if (builder.requestExecutorService != null) { - executor = builder.requestExecutorService; - } else { - executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); - } - } - - /** - * Adds a payload to the buffer - * - * @param payload an payload - */ - @Override - public abstract boolean add(TrackerPayload payload); - - /** - * Customize the emitter batch size to any valid integer greater than zero. - * Has no effect on SimpleEmitter - * - * @param batchSize number of events to collect before sending - */ - @Override - public abstract void setBatchSize(final int batchSize); - - /** - * Removes all payloads from the buffer and sends them - */ - @Override - public abstract void flushBuffer(); - - /** - * Gets the Emitter Batch Size - Will always be 1 for SimpleEmitter - * - * @return the batch size - */ - @Override - public abstract int getBatchSize(); - - /** - * Returns List of Payloads that are in the buffer. - * - * @return the buffered events - */ - @Override - public abstract List getBuffer(); - - /** - * Checks whether the response code was a success or not. - * - * @param code the response code - * @return whether it is in the success range - */ - protected boolean isSuccessfulSend(final int code) { - return code >= 200 && code < 300; - } - - /** - * Copied from `Executors.defaultThreadFactory()`. - * The only change is the generated name prefix. - */ - static class EmitterThreadFactory implements ThreadFactory { - private static final AtomicInteger poolNumber = new AtomicInteger(1); - private final ThreadGroup group; - private final AtomicInteger threadNumber = new AtomicInteger(1); - private final String namePrefix; - - EmitterThreadFactory() { - SecurityManager securityManager = System.getSecurityManager(); - this.group = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); - this.namePrefix = "snowplow-emitter-pool-" + poolNumber.getAndIncrement() + "-request-thread-"; - } - - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(this.group, runnable, this.namePrefix + this.threadNumber.getAndIncrement(), 0L); - if (thread.isDaemon()) { - thread.setDaemon(false); - } - - if (thread.getPriority() != 5) { - thread.setPriority(5); - } - - return thread; - } - } -} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 62e6380d..0f9d1727 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -16,15 +16,23 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; +import com.snowplowanalytics.snowplow.tracker.http.OkHttpClientAdapter; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import okhttp3.CookieJar; +import okhttp3.OkHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,42 +50,63 @@ * * If the buffer becomes full due to network problems, newer events will be lost. */ -public class BatchEmitter extends AbstractEmitter implements Closeable { +public class BatchEmitter implements Emitter, Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); private boolean isClosing = false; - + private final AtomicLong retryDelay; private int batchSize; + + private final HttpClientAdapter httpClientAdapter; + private final ScheduledExecutorService executor; private final EventStore eventStore; - private final AtomicLong retryDelay; private final List fatalResponseCodes; - public static abstract class Builder> extends AbstractEmitter.Builder { + public static abstract class Builder> { + protected abstract T self(); + private HttpClientAdapter httpClientAdapter; // Optional + private String collectorUrl = null; // Required if not specifying a httpClientAdapter private int batchSize = 50; // Optional private int bufferCapacity = Integer.MAX_VALUE; - private EventStore eventStore; - private List fatalResponseCodes; + private EventStore eventStore = null; // Optional + private List fatalResponseCodes = null; // Optional + private int threadCount = 50; // Optional + private CookieJar cookieJar = null; // Optional + private ScheduledExecutorService requestExecutorService = null; // Optional /** - * The default batch size is 50. + * Adds a custom HttpClientAdapter to the Emitter (default is OkHttpClientAdapter). * - * @param batchSize The count of events to send in one HTTP request + * @param httpClientAdapter the adapter to use * @return itself */ - public T batchSize(final int batchSize) { - this.batchSize = batchSize; + public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { + this.httpClientAdapter = httpClientAdapter; return self(); } + /** - * The default EventStore is InMemoryEventStore. + * Sets the emitter url for when a httpClientAdapter is not specified. + * It will be used to create the default OkHttpClientAdapter. * - * @param eventStore The EventStore to use + * @param collectorUrl the url for the default httpClientAdapter * @return itself */ - public T eventStore(final EventStore eventStore) { - this.eventStore = eventStore; + public T url(final String collectorUrl) { + this.collectorUrl = collectorUrl; + return self(); + } + + /** + * The default batch size is 50. + * + * @param batchSize The count of events to send in one HTTP request + * @return itself + */ + public T batchSize(final int batchSize) { + this.batchSize = batchSize; return self(); } @@ -93,6 +122,17 @@ public T bufferCapacity(final int bufferCapacity) { return self(); } + /** + * The default EventStore is InMemoryEventStore. + * + * @param eventStore The EventStore to use + * @return itself + */ + public T eventStore(final EventStore eventStore) { + this.eventStore = eventStore; + return self(); + } + /** * Provide a denylist of HTTP response codes. Retry will not be attempted if one of these codes * is received. The events in the request will be dropped, but the Emitter will continue trying @@ -106,6 +146,43 @@ public T fatalResponseCodes(final List fatalResponseCodes) { return self(); } + /** + * Sets the Thread Count for the ScheduledExecutorService (default is 50). + * + * @param threadCount the size of the thread pool + * @return itself + */ + public T threadCount(final int threadCount) { + this.threadCount = threadCount; + return self(); + } + + /** + * Set a custom ScheduledExecutorService to send http requests (default is ScheduledThreadPoolExecutor). + *

+ * Implementation note: Be aware that calling `close()` on a BatchEmitter instance + * has a side-effect and will shutdown that ExecutorService. + * + * @param requestExecutorService the ScheduledExecutorService to use + * @return itself + */ + public T requestExecutorService(final ScheduledExecutorService requestExecutorService) { + this.requestExecutorService = requestExecutorService; + return self(); + } + + /** + * Adds a custom CookieJar to be used with OkHttpClientAdapters. + * Will be ignored if a custom httpClientAdapter is provided. + * + * @param cookieJar the CookieJar to use + * @return itself + */ + public T cookieJar(final CookieJar cookieJar) { + this.cookieJar = cookieJar; + return self(); + } + public BatchEmitter build() { return new BatchEmitter(this); } @@ -123,24 +200,51 @@ public static Builder builder() { } protected BatchEmitter(final Builder builder) { - super(builder); + OkHttpClient client; // Precondition checks + Preconditions.checkArgument(builder.threadCount > 0, "threadCount must be greater than 0"); Preconditions.checkArgument(builder.batchSize > 0, "batchSize must be greater than 0"); - batchSize = builder.batchSize; - if (builder.eventStore == null) { - eventStore = new InMemoryEventStore(builder.bufferCapacity); + if (builder.httpClientAdapter != null) { + httpClientAdapter = builder.httpClientAdapter; } else { - eventStore = builder.eventStore; + Preconditions.checkNotNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); + + if (builder.cookieJar != null) { + client = new OkHttpClient.Builder() + .cookieJar(builder.cookieJar) + .build(); + } else { + client = new OkHttpClient.Builder().build(); + } + + httpClientAdapter = OkHttpClientAdapter.builder() // use okhttp as a default + .url(builder.collectorUrl) + .httpClient(client) + .build(); } + retryDelay = new AtomicLong(0L); + batchSize = builder.batchSize; + + if (builder.eventStore != null) { + eventStore = builder.eventStore; + } else { + eventStore = new InMemoryEventStore(builder.bufferCapacity); + } if (builder.fatalResponseCodes != null) { fatalResponseCodes = builder.fatalResponseCodes; } else { fatalResponseCodes = new ArrayList<>(); } + + if (builder.requestExecutorService != null) { + executor = builder.requestExecutorService; + } else { + executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); + } } /** @@ -213,6 +317,16 @@ long getRetryDelay() { return retryDelay.get(); } + /** + * Checks whether the response code was a success or not. + * + * @param code the response code + * @return whether it is in the success range + */ + protected boolean isSuccessfulSend(final int code) { + return code >= 200 && code < 300; + } + /** * Returns a Runnable POST Request operation * @@ -308,4 +422,34 @@ public void close() { } } } + + /** + * Copied from `Executors.defaultThreadFactory()`. + * The only change is the generated name prefix. + */ + static class EmitterThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + EmitterThreadFactory() { + SecurityManager securityManager = System.getSecurityManager(); + group = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); + namePrefix = "snowplow-emitter-pool-" + poolNumber.getAndIncrement() + "-request-thread-"; + } + + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0L); + if (thread.isDaemon()) { + thread.setDaemon(false); + } + + if (thread.getPriority() != 5) { + thread.setPriority(5); + } + + return thread; + } + } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index b2b21a08..14e80ff2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -33,7 +33,6 @@ public interface Emitter { /** * Customize the emitter batch size to any valid integer * greater than zero. - * Will only affect the BatchEmitter * * @param batchSize number of events to collect before * sending @@ -47,7 +46,6 @@ public interface Emitter { /** * Gets the Emitter Batch Size - * Will always be 1 for SimpleEmitter. Note that SimpleEmitter has been deprecated. * * @return the batch size */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java deleted file mode 100644 index 8f81dce0..00000000 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/SimpleEmitter.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package com.snowplowanalytics.snowplow.tracker.emitter; - -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import com.snowplowanalytics.snowplow.tracker.constants.Parameter; - -/** - * An emitter which sends events one-by-one as soon as they are received, via - * GET requests. The events are sent asynchronously. - * @deprecated Use the BatchEmitter, or create your own Emitter using the provided interface. - */ -@Deprecated -public class SimpleEmitter extends AbstractEmitter { - - private static final Logger LOGGER = LoggerFactory.getLogger(SimpleEmitter.class); - - public static abstract class Builder> extends AbstractEmitter.Builder { - public SimpleEmitter build() { - return new SimpleEmitter(this); - } - } - - private static class Builder2 extends Builder { - @Override - protected Builder2 self() { - return this; - } - } - - public static Builder builder() { - return new Builder2(); - } - - protected SimpleEmitter(final Builder builder) { - super(builder); - } - - /** - * Adds an event and instantly tries to send it. - * - * @param payload a payload - * @return true - */ - @Override - public boolean add(TrackerPayload payload) { - executor.execute(getGetRequestRunnable(payload)); - - // This result doesn't mean anything - // The return type is for BatchEmitter's benefit - return true; - } - - /** - * Sends all the buffered events, but SimpleEmitter does not buffer events. - * So has no effect - */ - @Override - public void flushBuffer() { - // Do nothing! - } - - /** - * Returns a Runnable GET Request operation - * - * @param payload the event to be sent - * @return the new Callable object - */ - private Runnable getGetRequestRunnable(final TrackerPayload payload) { - return new Runnable() { - @Override - public void run() { - payload.add(Parameter.DEVICE_SENT_TIMESTAMP, Long.toString(System.currentTimeMillis())); - final int code = httpClientAdapter.get(payload); - - // Process results - if (!isSuccessfulSend(code)) { - LOGGER.error("SimpleEmitter failed to send {} events: code: {}", 1, code); - } else { - LOGGER.debug("SimpleEmitter successfully sent {} events: code: {}", 1, code); - } - - } - }; - } - - /** - * Returns List of Events that are in the buffer. - * Always empty for SimpleEmitter - * - * @return the empty buffer - */ - @Override - public List getBuffer() { - return new ArrayList<>(); - } - - /** - * Customize the emitter batch size to any valid integer greater than zero. - * Has no effect on SimpleEmitter - * - * @param batchSize number of events to collect before sending - */ - @Override - public void setBatchSize(final int batchSize) { - if (batchSize != 1) { - LOGGER.debug("Noop. SimpleEmitter batch size must always be 1."); - } - } - - /** - * Gets the Emitter batch Size - Will always be 1 for SimpleEmitter - * - * @return the batch size - */ - @Override - public int getBatchSize() { - return 1; - } -} From 0918930283831ffe7411515093e860189ee894a0 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 1 Aug 2022 14:32:38 +0100 Subject: [PATCH 100/128] Add a maximum wait time and jitter to event sending retry (close #338) For PR #345 * Add jitter and maximum delay to request retry * Remove unused import * Use AtomicInteger properly --- .../tracker/emitter/BatchEmitter.java | 34 ++++++++++++++----- .../tracker/emitter/BatchEmitterTest.java | 14 +++++--- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 0f9d1727..645f4da5 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -21,7 +21,6 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.constants.Constants; @@ -54,7 +53,8 @@ public class BatchEmitter implements Emitter, Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(BatchEmitter.class); private boolean isClosing = false; - private final AtomicLong retryDelay; + private final AtomicInteger retryDelay; + private final int maximumRetryDelay = 600000; // ms (10 min) private int batchSize; private final HttpClientAdapter httpClientAdapter; @@ -225,7 +225,7 @@ protected BatchEmitter(final Builder builder) { .build(); } - retryDelay = new AtomicLong(0L); + retryDelay = new AtomicInteger(0); batchSize = builder.batchSize; if (builder.eventStore != null) { @@ -313,7 +313,7 @@ public int getBatchSize() { return batchSize; } - long getRetryDelay() { + int getRetryDelay() { return retryDelay.get(); } @@ -350,7 +350,7 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { // Process results if (isSuccessfulSend(code)) { LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", eventsInRequest.size(), code); - retryDelay.set(0L); + retryDelay.set(0); eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); } else if (fatalResponseCodes.contains(code)) { @@ -361,9 +361,9 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { LOGGER.error("BatchEmitter failed to send {} events: code: {}", eventsInRequest.size(), code); eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); - // exponentially increase retry backoff time after the first failure - if (!retryDelay.compareAndSet(0, 50L)) { - retryDelay.updateAndGet(currentDelay -> currentDelay * 2); + // exponentially increase retry backoff time after the first failure, up to the maximum wait time + if (!retryDelay.compareAndSet(0, 100)) { + retryDelay.updateAndGet(this::calculateRetryDelay); } } } catch (Exception e) { @@ -393,6 +393,24 @@ private SelfDescribingJson getFinalPost(final List events) { return new SelfDescribingJson(Constants.SCHEMA_PAYLOAD_DATA, toSendPayloads); } + private int calculateRetryDelay(int currentDelay) { + double newDelay; + double jitter = Math.random(); + int randomChoice = (Math.random() < 0.5) ? 0 : 1; + + switch (randomChoice) { + case 0: + newDelay = currentDelay * (2.0 + jitter); + break; + case 1: + newDelay = currentDelay * (2.0 - jitter); + break; + default: + newDelay = currentDelay; + } + return Math.min((int) newDelay, maximumRetryDelay); + } + /** * Attempt to send all remaining events, then shut down the ExecutorService. * diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index bd0bb05b..fa17b219 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -297,13 +297,17 @@ public void eventSendingFailureIncreasesBackoffTime() throws InterruptedExceptio .batchSize(1) .build(); - List payloads = createPayloads(2); - for (TrackerPayload payload : payloads) { - emitter.add(payload); - } + emitter.add(createPayload()); + Thread.sleep(500); + + int firstDelay = emitter.getRetryDelay(); + Assert.assertNotEquals(0, firstDelay); + + emitter.add(createPayload()); Thread.sleep(500); - Assert.assertEquals(100, emitter.getRetryDelay()); + int secondDelay = emitter.getRetryDelay(); + Assert.assertTrue(secondDelay > firstDelay); } @Test From 5460a97bae3ad95032d35c25b3d181cafe69f92c Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Fri, 12 Aug 2022 14:05:55 +0100 Subject: [PATCH 101/128] Remove Guava dependency (close #320) For PR #351. * Change Preconditions to normal Java * Replace ImmutableMap and Lists --- build.gradle | 3 - .../main/java/com/snowplowanalytics/Main.java | 8 +- .../snowplow/tracker/Tracker.java | 16 +- .../tracker/emitter/BatchEmitter.java | 19 +- .../tracker/events/AbstractEvent.java | 6 +- .../tracker/events/EcommerceTransaction.java | 14 +- .../events/EcommerceTransactionItem.java | 22 +- .../snowplow/tracker/events/PageView.java | 11 +- .../snowplow/tracker/events/ScreenView.java | 6 +- .../tracker/events/SelfDescribing.java | 7 +- .../snowplow/tracker/events/Structured.java | 17 +- .../snowplow/tracker/events/Timing.java | 18 +- .../http/AbstractHttpClientAdapter.java | 6 +- .../tracker/http/ApacheHttpClientAdapter.java | 6 +- .../tracker/http/OkHttpClientAdapter.java | 6 +- .../tracker/payload/SelfDescribingJson.java | 10 +- .../snowplow/tracker/TrackerTest.java | 352 +++++++++--------- .../tracker/emitter/BatchEmitterTest.java | 4 +- .../tracker/http/HttpClientAdapterTest.java | 9 +- 19 files changed, 266 insertions(+), 274 deletions(-) diff --git a/build.gradle b/build.gradle index 60f7ca9d..6e99d26f 100644 --- a/build.gradle +++ b/build.gradle @@ -75,9 +75,6 @@ dependencies { // Jackson JSON processor api 'com.fasterxml.jackson.core:jackson-databind:2.13.3' - // Preconditions - api 'com.google.guava:guava:31.1-jre' - // Testing libraries testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testCompileOnly 'junit:junit:4.13.2' diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 28772477..aae53326 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -20,10 +20,10 @@ import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; -import java.util.List; +import java.util.Collections; import static java.util.Collections.singletonList; +import java.util.List; -import com.google.common.collect.ImmutableMap; public class Main { @@ -61,7 +61,7 @@ public static void main(String[] args) throws InterruptedException { List context = singletonList( new SelfDescribingJson( "iglu:com.snowplowanalytics.iglu/anything-c/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar"))); + Collections.singletonMap("foo", "bar"))); // This is an example of a eventSubject for adding user data Subject eventSubject = new Subject.SubjectBuilder().build(); @@ -111,7 +111,7 @@ public static void main(String[] args) throws InterruptedException { SelfDescribing selfDescribing = SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.iglu/anything-a/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar") + Collections.singletonMap("foo", "bar") )) .customContext(context) .build(); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index c9d69872..1b3168b4 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -12,8 +12,6 @@ */ package com.snowplowanalytics.snowplow.tracker; -import com.google.common.base.Preconditions; - import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; @@ -41,11 +39,15 @@ public class Tracker { private Tracker(TrackerBuilder builder) { // Precondition checks - Preconditions.checkNotNull(builder.emitter); - Preconditions.checkNotNull(builder.namespace); - Preconditions.checkNotNull(builder.appId); - Preconditions.checkArgument(!builder.namespace.isEmpty(), "namespace cannot be empty"); - Preconditions.checkArgument(!builder.appId.isEmpty(), "appId cannot be empty"); + Objects.requireNonNull(builder.emitter); + Objects.requireNonNull(builder.namespace); + Objects.requireNonNull(builder.appId); + if (builder.namespace.isEmpty()) { + throw new IllegalArgumentException("namespace cannot be empty"); + } + if (builder.appId.isEmpty()) { + throw new IllegalArgumentException("appId cannot be empty"); + } this.parameters = new TrackerParameters(builder.appId, builder.platform, builder.namespace, Version.TRACKER, builder.base64Encoded); this.emitter = builder.emitter; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 645f4da5..f407f557 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -16,13 +16,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import com.google.common.base.Preconditions; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; @@ -203,13 +203,20 @@ protected BatchEmitter(final Builder builder) { OkHttpClient client; // Precondition checks - Preconditions.checkArgument(builder.threadCount > 0, "threadCount must be greater than 0"); - Preconditions.checkArgument(builder.batchSize > 0, "batchSize must be greater than 0"); + if (builder.threadCount <= 0) { + throw new IllegalArgumentException("threadCount must be greater than 0"); + } + if (builder.batchSize <= 0) { + throw new IllegalArgumentException("batchSize must be greater than 0"); + } + if (builder.bufferCapacity <= 0) { + throw new IllegalArgumentException("bufferCapacity must be greater than 0"); + } if (builder.httpClientAdapter != null) { httpClientAdapter = builder.httpClientAdapter; } else { - Preconditions.checkNotNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); + Objects.requireNonNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); if (builder.cookieJar != null) { client = new OkHttpClient.Builder() @@ -299,7 +306,9 @@ public List getBuffer() { */ @Override public void setBatchSize(final int batchSize) { - Preconditions.checkArgument(batchSize > 0, "batchSize must be greater than 0"); + if (batchSize <= 0) { + throw new IllegalArgumentException("batchSize must be greater than 0"); + } this.batchSize = batchSize; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index d77e2a28..9a9dc141 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -16,9 +16,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; - -// Google -import com.google.common.base.Preconditions; +import java.util.Objects; // This library import com.snowplowanalytics.snowplow.tracker.Subject; @@ -104,7 +102,7 @@ public static Builder builder() { protected AbstractEvent(Builder builder) { // Precondition checks - Preconditions.checkNotNull(builder.context); + Objects.requireNonNull(builder.context); this.context = builder.context; this.trueTimestamp = builder.trueTimestamp; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 399f3847..22d82b21 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -16,9 +16,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - -// Google -import com.google.common.base.Preconditions; +import java.util.Objects; // This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; @@ -214,10 +212,12 @@ protected EcommerceTransaction(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.orderId); - Preconditions.checkNotNull(builder.totalValue); - Preconditions.checkNotNull(builder.items); - Preconditions.checkArgument(!builder.orderId.isEmpty(), "orderId cannot be empty"); + Objects.requireNonNull(builder.orderId); + Objects.requireNonNull(builder.totalValue); + Objects.requireNonNull(builder.items); + if (builder.orderId.isEmpty()) { + throw new IllegalArgumentException("orderId cannot be empty"); + } this.orderId = builder.orderId; this.totalValue = builder.totalValue; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index 9b12d34b..0f72ca3f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -12,15 +12,13 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -// Google - -import com.google.common.base.Preconditions; - // This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import java.util.Objects; + /** * Constructs an EcommerceTransactionItem object. *

@@ -155,12 +153,16 @@ protected EcommerceTransactionItem(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.itemId); - Preconditions.checkNotNull(builder.sku); - Preconditions.checkNotNull(builder.price); - Preconditions.checkNotNull(builder.quantity); - Preconditions.checkArgument(!builder.itemId.isEmpty(), "itemId cannot be empty"); - Preconditions.checkArgument(!builder.sku.isEmpty(), "sku cannot be empty"); + Objects.requireNonNull(builder.itemId); + Objects.requireNonNull(builder.sku); + Objects.requireNonNull(builder.price); + Objects.requireNonNull(builder.quantity); + if (builder.itemId.isEmpty()) { + throw new IllegalArgumentException("itemId cannot be empty"); + } + if (builder.sku.isEmpty()) { + throw new IllegalArgumentException("sku cannot be empty"); + } this.itemId = builder.itemId; this.sku = builder.sku; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index da9becbb..536a6685 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -12,14 +12,13 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -// Google -import com.google.common.base.Preconditions; - // This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import java.util.Objects; + /** * Constructs a PageView event object. * @@ -90,8 +89,10 @@ protected PageView(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.pageUrl); - Preconditions.checkArgument(!builder.pageUrl.isEmpty(), "pageUrl cannot be empty"); + Objects.requireNonNull(builder.pageUrl); + if (builder.pageUrl.isEmpty()) { + throw new IllegalArgumentException("pageUrl cannot be empty"); + } this.pageUrl = builder.pageUrl; this.pageTitle = builder.pageTitle; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index 3a42020b..b2445f73 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -12,8 +12,6 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -import com.google.common.base.Preconditions; - import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -77,7 +75,9 @@ protected ScreenView(Builder builder) { super(builder); // Precondition checks - Preconditions.checkArgument(builder.name != null || builder.id != null); + if (builder.name == null || builder.id == null) { + throw new IllegalArgumentException(); + } this.name = builder.name; this.id = builder.id; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java index e9732e9f..b2a4b90f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java @@ -12,15 +12,14 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -// Google -import com.google.common.base.Preconditions; - // This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import java.util.Objects; + /** * Constructs a SelfDescribing event object. * @@ -70,7 +69,7 @@ protected SelfDescribing(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.eventData); + Objects.requireNonNull(builder.eventData); this.eventData = builder.eventData; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index d4361b65..953a94a0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -12,14 +12,13 @@ */ package com.snowplowanalytics.snowplow.tracker.events; -// Google -import com.google.common.base.Preconditions; - // This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import java.util.Objects; + /** * Constructs a Structured event object. * @@ -124,10 +123,14 @@ protected Structured(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.category); - Preconditions.checkNotNull(builder.action); - Preconditions.checkArgument(!builder.category.isEmpty(), "category cannot be empty"); - Preconditions.checkArgument(!builder.action.isEmpty(), "action cannot be empty"); + Objects.requireNonNull(builder.category); + Objects.requireNonNull(builder.action); + if (builder.category.isEmpty()) { + throw new IllegalArgumentException("category cannot be empty"); + } + if (builder.action.isEmpty()) { + throw new IllegalArgumentException("action cannot be empty"); + } this.category = builder.category; this.action = builder.action; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index 7bed6f3e..729f9074 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -14,9 +14,7 @@ // Java import java.util.LinkedHashMap; - -// Google -import com.google.common.base.Preconditions; +import java.util.Objects; // This library import com.snowplowanalytics.snowplow.tracker.constants.Parameter; @@ -106,11 +104,15 @@ protected Timing(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.category); - Preconditions.checkNotNull(builder.timing); - Preconditions.checkNotNull(builder.variable); - Preconditions.checkArgument(!builder.category.isEmpty(), "category cannot be empty"); - Preconditions.checkArgument(!builder.variable.isEmpty(), "variable cannot be empty"); + Objects.requireNonNull(builder.category); + Objects.requireNonNull(builder.timing); + Objects.requireNonNull(builder.variable); + if (builder.category.isEmpty()) { + throw new IllegalArgumentException("category cannot be empty"); + } + if (builder.variable.isEmpty()) { + throw new IllegalArgumentException("variable cannot be empty"); + } this.category = builder.category; this.variable = builder.variable; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index 451d5aa7..bfe43cda 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -12,8 +12,6 @@ */ package com.snowplowanalytics.snowplow.tracker.http; -import com.google.common.base.Preconditions; - import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.Utils; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -56,7 +54,9 @@ public static Builder builder() { protected AbstractHttpClientAdapter(Builder builder) { // Precondition checks - Preconditions.checkArgument(Utils.isValidUrl(builder.url)); + if (!Utils.isValidUrl(builder.url)) { + throw new IllegalArgumentException(); + } this.url = builder.url; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 5620d88a..417918a1 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -12,8 +12,6 @@ */ package com.snowplowanalytics.snowplow.tracker.http; -import com.google.common.base.Preconditions; - import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; @@ -26,6 +24,8 @@ import com.snowplowanalytics.snowplow.tracker.constants.Constants; +import java.util.Objects; + /** * A HttpClient built using Apache to send events via * GET or POST requests. @@ -68,7 +68,7 @@ protected ApacheHttpClientAdapter(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.httpClient); + Objects.requireNonNull(builder.httpClient); this.httpClient = builder.httpClient; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index b476fa71..4db80155 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -14,9 +14,7 @@ // Java import java.io.IOException; - -// Google -import com.google.common.base.Preconditions; +import java.util.Objects; // SquareUp import okhttp3.*; @@ -71,7 +69,7 @@ protected OkHttpClientAdapter(Builder builder) { super(builder); // Precondition checks - Preconditions.checkNotNull(builder.httpClient); + Objects.requireNonNull(builder.httpClient); httpClient = builder.httpClient; } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index 155228dc..542b71c7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -14,8 +14,7 @@ import java.util.LinkedHashMap; import java.util.Map; - -import com.google.common.base.Preconditions; +import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,8 +93,11 @@ public SelfDescribingJson(String schema, Object data) { * @return this SelfDescribingJson */ public SelfDescribingJson setSchema(String schema) { - Preconditions.checkNotNull(schema, "schema cannot be null"); - Preconditions.checkArgument(!schema.isEmpty(), "schema cannot be empty."); + Objects.requireNonNull(schema, "schema cannot be null"); + if (schema.isEmpty()) { + throw new IllegalArgumentException("schema cannot be empty"); + } + payload.put(Parameter.SCHEMA, schema); return this; } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 0a9acc1a..63f22620 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -19,8 +19,6 @@ import org.junit.Test; import static org.junit.Assert.*; -import com.google.common.collect.ImmutableMap; - import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; import com.snowplowanalytics.snowplow.tracker.events.*; @@ -60,7 +58,7 @@ public void setUp() { .base64(false) .build(); tracker.getSubject().setTimezone("Etc/UTC"); - contexts = singletonList(new SelfDescribingJson("schema", ImmutableMap.of("foo", "bar"))); + contexts = singletonList(new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar"))); } // --- Event Tests @@ -71,7 +69,7 @@ public void testTrackReturnsEventIdIfSuccessful() throws InterruptedException { List result = tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar") + Collections.singletonMap("foo", "bar") )) .build()); @@ -107,7 +105,7 @@ public void flushBuffer() {} List result = tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar") + Collections.singletonMap("foo", "bar") )) .build()); @@ -154,46 +152,44 @@ public void testEcommerceEvent() throws InterruptedException { assertEquals(2, results.size()); Map result1 = results.get(0).getMap(); - Map expected1 = ImmutableMap.builder() - .put("e", "tr") - .put("tr_cu", "currency") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("tr_sh", "3.0") - .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("tr_co", "country") - .put("tv", Version.TRACKER) - .put("p", "srv") - .put("tr_tx", "2.0") - .put("tr_af", "affiliation") - .put("tr_id", "order_id") - .put("tr_tt", "1.0") - .put("tr_ci", "city") - .put("tr_st", "state") - .build(); + Map expected1 = new HashMap<>(); + expected1.put("e", "tr"); + expected1.put("tr_cu", "currency"); + expected1.put("co", EXPECTED_CONTEXTS); + expected1.put("tna", "AF003"); + expected1.put("aid", "cloudfront"); + expected1.put("tr_sh", "3.0"); + expected1.put("ttm", "456789"); + expected1.put("tz", "Etc/UTC"); + expected1.put("tr_co", "country"); + expected1.put("tv", Version.TRACKER); + expected1.put("p", "srv"); + expected1.put("tr_tx", "2.0"); + expected1.put("tr_af", "affiliation"); + expected1.put("tr_id", "order_id"); + expected1.put("tr_tt", "1.0"); + expected1.put("tr_ci", "city"); + expected1.put("tr_st", "state"); assertTrue(result1.entrySet().containsAll(expected1.entrySet())); Map result2 = results.get(1).getMap(); - Map expected2 = ImmutableMap.builder() - .put("ti_nm", "name") - .put("ti_id", "order_id") - .put("e", "ti") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("ti_cu", "currency") - .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("ti_pr", "1.0") - .put("ti_qu", "2") - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("ti_ca", "category") - .put("ti_sk", "sku") - .build(); + Map expected2 = new HashMap<>(); + expected2.put("ti_nm", "name"); + expected2.put("ti_id", "order_id"); + expected2.put("e", "ti"); + expected2.put("co", EXPECTED_CONTEXTS); + expected2.put("tna", "AF003"); + expected2.put("aid", "cloudfront"); + expected2.put("ti_cu", "currency"); + expected2.put("ttm", "456789"); + expected2.put("tz", "Etc/UTC"); + expected2.put("ti_pr", "1.0"); + expected2.put("ti_qu", "2"); + expected2.put("p", "srv"); + expected2.put("tv", Version.TRACKER); + expected2.put("ti_ca", "category"); + expected2.put("ti_sk", "sku"); assertTrue(result2.entrySet().containsAll(expected2.entrySet())); } @@ -219,23 +215,22 @@ public void testEcommerceTransactionItemAlone() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("ti_nm", "name") - .put("ti_id", "order_id") - .put("e", "ti") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("ti_cu", "currency") - .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("ti_pr", "1.0") - .put("ti_qu", "2") - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("ti_ca", "category") - .put("ti_sk", "sku") - .build(); + Map expected = new HashMap<>(); + expected.put("ti_nm", "name"); + expected.put("ti_id", "order_id"); + expected.put("e", "ti"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("aid", "cloudfront"); + expected.put("ti_cu", "currency"); + expected.put("ttm", "456789"); + expected.put("tz", "Etc/UTC"); + expected.put("ti_pr", "1.0"); + expected.put("ti_qu", "2"); + expected.put("p", "srv"); + expected.put("tv", Version.TRACKER); + expected.put("ti_ca", "category"); + expected.put("ti_sk", "sku"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -246,7 +241,7 @@ public void testSelfDescribingEventWithContext() throws InterruptedException { tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar") + Collections.singletonMap("foo", "bar") )) .customContext(contexts) .trueTimestamp(456789L) @@ -256,17 +251,16 @@ public void testSelfDescribingEventWithContext() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}") - .put("ttm", "456789") - .put("aid", "cloudfront") - .build(); + Map expected = new HashMap<>(); + expected.put("p", "srv"); + expected.put("tv", Version.TRACKER); + expected.put("e", "ue"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("tz", "Etc/UTC"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}"); + expected.put("ttm", "456789"); + expected.put("aid", "cloudfront"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -277,7 +271,7 @@ public void testSelfDescribingEventWithoutContext() throws InterruptedException tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", - ImmutableMap.of("foo", "baær") + Collections.singletonMap("foo", "baær") )) .trueTimestamp(456789L) .build()); @@ -286,16 +280,15 @@ public void testSelfDescribingEventWithoutContext() throws InterruptedException Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"baær\"}}}") - .put("ttm", "456789") - .put("aid", "cloudfront") - .build(); + Map expected = new HashMap<>(); + expected.put("p", "srv"); + expected.put("tv", Version.TRACKER); + expected.put("e", "ue"); + expected.put("tna", "AF003"); + expected.put("tz", "Etc/UTC"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"baær\"}}}"); + expected.put("ttm", "456789"); + expected.put("aid", "cloudfront"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -306,7 +299,7 @@ public void testSelfDescribingEventWithoutTrueTimestamp() throws InterruptedExce tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar") + Collections.singletonMap("foo", "bar") )) .build()); @@ -314,15 +307,14 @@ public void testSelfDescribingEventWithoutTrueTimestamp() throws InterruptedExce Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}") - .put("aid", "cloudfront") - .build(); + Map expected = new HashMap<>(); + expected.put("p", "srv"); + expected.put("tv", Version.TRACKER); + expected.put("e", "ue"); + expected.put("tna", "AF003"); + expected.put("tz", "Etc/UTC"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}"); + expected.put("aid", "cloudfront"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -348,19 +340,18 @@ public void testTrackPageView() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("e", "pv") - .put("page", "title") - .put("tv", Version.TRACKER) - .put("p", "srv") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("refr", "referer") - .put("url", "url") - .build(); + Map expected = new HashMap<>(); + expected.put("ttm", "456789"); + expected.put("tz", "Etc/UTC"); + expected.put("e", "pv"); + expected.put("page", "title"); + expected.put("tv", Version.TRACKER); + expected.put("p", "srv"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("aid", "cloudfront"); + expected.put("refr", "referer"); + expected.put("url", "url"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -378,7 +369,7 @@ public void testTrackTwoEvents() throws InterruptedException { tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( "iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0", - ImmutableMap.of("foo", "bar") + Collections.singletonMap("foo", "bar") )) .trueTimestamp(456789L) .build()); @@ -390,32 +381,30 @@ public void testTrackTwoEvents() throws InterruptedException { assertEquals(2, results.size()); Map result1 = results.get(0).getMap(); - Map expected1 = ImmutableMap.builder() - .put("ttm", "123456") - .put("tz", "Etc/UTC") - .put("e", "pv") - .put("page", "title") - .put("tv", Version.TRACKER) - .put("p", "srv") - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("refr", "referer") - .put("url", "url") - .build(); - - assertTrue(result1.entrySet().containsAll(expected1.entrySet())); + Map expected = new HashMap<>(); + expected.put("ttm", "123456"); + expected.put("tz", "Etc/UTC"); + expected.put("e", "pv"); + expected.put("page", "title"); + expected.put("tv", Version.TRACKER); + expected.put("p", "srv"); + expected.put("tna", "AF003"); + expected.put("aid", "cloudfront"); + expected.put("refr", "referer"); + expected.put("url", "url"); + + assertTrue(result1.entrySet().containsAll(expected.entrySet())); Map result2 = results.get(1).getMap(); - Map expected2 = ImmutableMap.builder() - .put("ttm", "456789") - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}") - .put("aid", "cloudfront") - .build(); + Map expected2 = new HashMap<>(); + expected2.put("ttm", "456789"); + expected2.put("p", "srv"); + expected2.put("tv", Version.TRACKER); + expected2.put("e", "ue"); + expected2.put("tna", "AF003"); + expected2.put("tz", "Etc/UTC"); + expected2.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/example/jsonschema/1-0-0\",\"data\":{\"foo\":\"bar\"}}}"); + expected2.put("aid", "cloudfront"); assertTrue(result2.entrySet().containsAll(expected2.entrySet())); } @@ -434,17 +423,16 @@ public void testTrackScreenView() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("e", "ue") - .put("tv", Version.TRACKER) - .put("p", "srv") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") - .build(); + Map expected = new HashMap<>(); + expected.put("ttm", "456789"); + expected.put("tz", "Etc/UTC"); + expected.put("e", "ue"); + expected.put("tv", Version.TRACKER); + expected.put("p", "srv"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("aid", "cloudfront"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -462,16 +450,15 @@ public void testTrackScreenViewWithTimestamp() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("ttm", "456789") - .put("tz", "Etc/UTC") - .put("e", "ue") - .put("tv", Version.TRACKER) - .put("p", "srv") - .put("tna", "AF003") - .put("aid", "cloudfront") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") - .build(); + Map expected = new HashMap<>(); + expected.put("ttm", "456789"); + expected.put("tz", "Etc/UTC"); + expected.put("e", "ue"); + expected.put("tv", Version.TRACKER); + expected.put("p", "srv"); + expected.put("tna", "AF003"); + expected.put("aid", "cloudfront"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -490,17 +477,16 @@ public void testTrackScreenViewWithDefaultContextAndTimestamp() throws Interrupt Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}") - .put("ttm", "456789") - .put("aid", "cloudfront") - .build(); + Map expected = new HashMap<>(); + expected.put("p", "srv"); + expected.put("tv", Version.TRACKER); + expected.put("e", "ue"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("tz", "Etc/UTC"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0\",\"data\":{\"id\":\"id\",\"name\":\"name\"}}}"); + expected.put("ttm", "456789"); + expected.put("aid", "cloudfront"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -521,17 +507,16 @@ public void testTrackTiming() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("p", "srv") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") - .put("ttm", "456789") - .put("aid", "cloudfront") - .build(); + Map expected = new HashMap<>(); + expected.put("p", "srv"); + expected.put("tv", Version.TRACKER); + expected.put("e", "ue"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("tz", "Etc/UTC"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}"); + expected.put("ttm", "456789"); + expected.put("aid", "cloudfront"); assertTrue(result.entrySet().containsAll(expected.entrySet())); } @@ -558,18 +543,17 @@ public void testTrackTimingWithSubject() throws InterruptedException { Thread.sleep(500); Map result = mockEmitter.eventList.get(0).getMap(); - Map expected = ImmutableMap.builder() - .put("p", "srv") - .put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}") - .put("tv", Version.TRACKER) - .put("e", "ue") - .put("ip", "127.0.0.1") - .put("co", EXPECTED_CONTEXTS) - .put("tna", "AF003") - .put("tz", "Etc/UTC") - .put("ttm", "456789") - .put("aid", "cloudfront") - .build(); + Map expected = new HashMap<>(); + expected.put("p", "srv"); + expected.put("ue_pr", "{\"schema\":\"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0\",\"data\":{\"schema\":\"iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0\",\"data\":{\"category\":\"category\",\"label\":\"label\",\"timing\":10,\"variable\":\"variable\"}}}"); + expected.put("tv", Version.TRACKER); + expected.put("e", "ue"); + expected.put("ip", "127.0.0.1"); + expected.put("co", EXPECTED_CONTEXTS); + expected.put("tna", "AF003"); + expected.put("tz", "Etc/UTC"); + expected.put("ttm", "456789"); + expected.put("aid", "cloudfront"); assertTrue(result.entrySet().containsAll(expected.entrySet())); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index fa17b219..0095d05b 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -18,8 +18,6 @@ import java.util.Objects; import java.util.regex.Pattern; -import com.google.common.collect.Lists; - import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import org.junit.Assert; import org.junit.Before; @@ -368,7 +366,7 @@ private TrackerPayload createPayload() { } private List createPayloads(int numPayloads) { - final List payloads = Lists.newArrayList(); + final List payloads = new ArrayList<>(); for (int i = 0; i < numPayloads; i++) { payloads.add(createPayload()); } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 3d7ade9f..df84a82f 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -15,12 +15,9 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; -import java.util.EnumSet; +import java.util.Collections; import java.util.concurrent.TimeUnit; -import com.google.common.collect.ImmutableMap; - -import com.snowplowanalytics.snowplow.tracker.payload.Payload; import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -112,7 +109,7 @@ public void post_withSuccessfulStatusCode_isOk() throws InterruptedException { mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // When - adapter.post(new SelfDescribingJson("schema", ImmutableMap.of("foo", "bar"))); + adapter.post(new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar"))); // Then assertEquals(1, mockWebServer.getRequestCount()); @@ -148,7 +145,7 @@ public void testRequestWithCookies() throws IOException, InterruptedException { mockWebServer.enqueue(new MockResponse().addHeader("Set-Cookie", "sp=test")); - SelfDescribingJson payload = new SelfDescribingJson("schema", ImmutableMap.of("foo", "bar")); + SelfDescribingJson payload = new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar")); adapter.post(payload); adapter.post(payload); From 4a52f2612745c5bd9d593a54b755e90dec1a094e Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Fri, 12 Aug 2022 14:15:18 +0100 Subject: [PATCH 102/128] Standardise API for Tracker and Subject Builders (close #302) For PR #350 * Add builder method to Tracker * Add builder method to Subject * Remove unused import --- .../main/java/com/snowplowanalytics/Main.java | 4 +-- .../snowplow/tracker/Subject.java | 4 +++ .../snowplow/tracker/Tracker.java | 4 +++ .../snowplow/tracker/SubjectTest.java | 24 ++++++++-------- .../snowplow/tracker/TrackerTest.java | 28 +++++++++---------- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index aae53326..a0bcfaa6 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -49,7 +49,7 @@ public static void main(String[] args) throws InterruptedException { .build(); // now we have the emitter, we need a tracker to turn our events into something a Snowplow collector can understand - final Tracker tracker = new Tracker.TrackerBuilder(emitter, namespace, appId) + final Tracker tracker = Tracker.builder(emitter, namespace, appId) .base64(true) .platform(DevicePlatform.ServerSideApp) .build(); @@ -64,7 +64,7 @@ public static void main(String[] args) throws InterruptedException { Collections.singletonMap("foo", "bar"))); // This is an example of a eventSubject for adding user data - Subject eventSubject = new Subject.SubjectBuilder().build(); + Subject eventSubject = Subject.builder().build(); eventSubject.setUserId("example@snowplowanalytics.com"); eventSubject.setLanguage("EN"); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index fb803f9a..fd298035 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -55,6 +55,10 @@ public Subject(Subject subject){ this.standardPairs.putAll(subject.getSubject()); } + public static SubjectBuilder builder() { + return new SubjectBuilder(); + } + /** * Builder for the Subject */ diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 1b3168b4..72555d7a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -55,6 +55,10 @@ private Tracker(TrackerBuilder builder) { } + public static TrackerBuilder builder(Emitter emitter, String namespace, String appId) { + return new TrackerBuilder(emitter, namespace, appId); + } + /** * Builder for the Tracker */ diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 9ef9352e..4e865ad8 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -24,84 +24,84 @@ public class SubjectTest { @Test public void testSetUserId() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setUserId("user1"); assertEquals("user1", subject.getSubject().get("uid")); } @Test public void testSetScreenResolution() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setScreenResolution(100, 150); assertEquals("100x150", subject.getSubject().get("res")); } @Test public void testSetViewPort() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setViewPort(150, 100); assertEquals("150x100", subject.getSubject().get("vp")); } @Test public void testSetColorDepth() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setColorDepth(10); assertEquals("10", subject.getSubject().get("cd")); } @Test public void testSetTimezone2() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setTimezone("America/Toronto"); assertEquals("America/Toronto", subject.getSubject().get("tz")); } @Test public void testSetLanguage() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setLanguage("EN"); assertEquals("EN", subject.getSubject().get("lang")); } @Test public void testSetIpAddress() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setIpAddress("127.0.0.1"); assertEquals("127.0.0.1", subject.getSubject().get("ip")); } @Test public void testSetUseragent() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setUseragent("useragent"); assertEquals("useragent", subject.getSubject().get("ua")); } @Test public void testSetDomainUserId() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setDomainUserId("duid"); assertEquals("duid", subject.getSubject().get("duid")); } @Test public void testSetNetworkUserId() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setNetworkUserId("nuid"); assertEquals("nuid", subject.getSubject().get("tnuid")); } @Test public void testSetDomainSessionId() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); subject.setDomainSessionId("sessionid"); assertEquals("sessionid", subject.getSubject().get("sid")); } @Test public void testGetSubject() { - Subject subject = new Subject.SubjectBuilder().build(); + Subject subject = Subject.builder().build(); Map expected = new HashMap<>(); subject.setTimezone("America/Toronto"); subject.setUserId("user1"); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 63f22620..ba7f544e 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -53,8 +53,8 @@ public void flushBuffer() {} @Before public void setUp() { mockEmitter = new MockEmitter(); - tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") - .subject(new Subject.SubjectBuilder().build()) + tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") + .subject(Subject.builder().build()) .base64(false) .build(); tracker.getSubject().setTimezone("Etc/UTC"); @@ -100,7 +100,7 @@ public void flushBuffer() {} public List getBuffer() { return null; } } FailingMockEmitter failingMockEmitter = new FailingMockEmitter(); - tracker = new Tracker.TrackerBuilder(failingMockEmitter, "AF003", "cloudfront").build(); + tracker = Tracker.builder(failingMockEmitter, "AF003", "cloudfront").build(); List result = tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( @@ -321,8 +321,8 @@ public void testSelfDescribingEventWithoutTrueTimestamp() throws InterruptedExce @Test public void testTrackPageView() throws InterruptedException { - tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") - .subject(new Subject.SubjectBuilder().build()) + tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") + .subject(Subject.builder().build()) .base64(false) .build(); tracker.getSubject().setTimezone("Etc/UTC"); @@ -524,7 +524,7 @@ public void testTrackTiming() throws InterruptedException { @Test public void testTrackTimingWithSubject() throws InterruptedException { // Make Subject - Subject s1 = new Subject.SubjectBuilder().build(); + Subject s1 = Subject.builder().build(); s1.setIpAddress("127.0.0.1"); s1.setTimezone("Etc/UTC"); @@ -563,13 +563,13 @@ public void testTrackTimingWithSubject() throws InterruptedException { @Test public void testGetTrackerVersion() { - Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); + Tracker tracker = Tracker.builder(mockEmitter, "namespace", "an-app-id").build(); assertEquals("java-0.12.2", tracker.getTrackerVersion()); } @Test public void testSetDefaultPlatform() { - Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") + Tracker tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") .platform(DevicePlatform.Desktop) .build(); assertEquals(DevicePlatform.Desktop, tracker.getPlatform()); @@ -580,13 +580,13 @@ public void testSetSubject() { // Subject objects always have timezone set TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); - Subject s1 = new Subject.SubjectBuilder().build(); + Subject s1 = Subject.builder().build(); s1.setLanguage("EN"); - Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") + Tracker tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") .subject(s1) .build(); - Subject s2 = new Subject.SubjectBuilder().build(); + Subject s2 = Subject.builder().build(); s2.setColorDepth(24); tracker.setSubject(s2); @@ -599,7 +599,7 @@ public void testSetSubject() { @Test public void testSetBase64Encoded() { - Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "cloudfront") + Tracker tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") .base64(false) .build(); assertFalse(tracker.getBase64Encoded()); @@ -607,13 +607,13 @@ public void testSetBase64Encoded() { @Test public void testSetAppId() { - Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "AF003", "an-app-id").build(); + Tracker tracker = Tracker.builder(mockEmitter, "AF003", "an-app-id").build(); assertEquals("an-app-id", tracker.getAppId()); } @Test public void testSetNamespace() { - Tracker tracker = new Tracker.TrackerBuilder(mockEmitter, "namespace", "an-app-id").build(); + Tracker tracker = Tracker.builder(mockEmitter, "namespace", "an-app-id").build(); assertEquals("namespace", tracker.getNamespace()); } } From 42f40d090c7b2ce10751cf530efcb399354e9de4 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 15 Aug 2022 11:53:37 +0100 Subject: [PATCH 103/128] Add admin workflow for automatic issue labelling (close #346) For PR #347 * Add admin workflow for automatic issue labelling (close #346) * Add job to link issues with PRs --- .github/workflows/admin.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/admin.yml diff --git a/.github/workflows/admin.yml b/.github/workflows/admin.yml new file mode 100644 index 00000000..6cbf6559 --- /dev/null +++ b/.github/workflows/admin.yml @@ -0,0 +1,36 @@ +name: Admin + +on: + create: + pull_request: + types: + - opened + branches: + - 'release/**' + push: + branches: + - "release/**" + +jobs: + update-labels: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Update issue status labels + uses: snowplow-incubator/labels-helper-action@v1 + env: + ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + link-pr-issue: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Match the issue to the PR + uses: snowplow-incubator/pull-request-helper-action@v1 + env: + ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e895fb1bf9545e02e1f12f28a6c7ac22208e6b6b Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 15 Aug 2022 12:13:27 +0100 Subject: [PATCH 104/128] Set default HTTP status codes not to retry on (close #337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For PR #349 * Improve retry status code handling * Remove extra semicolon Co-authored-by: Matúš Tomlein --- .../tracker/emitter/BatchEmitter.java | 46 +++++++++------ .../tracker/emitter/BatchEmitterTest.java | 59 ++++++++++++++++--- 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index f407f557..3c532aa2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -13,10 +13,7 @@ package com.snowplowanalytics.snowplow.tracker.emitter; import java.io.Closeable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; @@ -60,7 +57,7 @@ public class BatchEmitter implements Emitter, Closeable { private final HttpClientAdapter httpClientAdapter; private final ScheduledExecutorService executor; private final EventStore eventStore; - private final List fatalResponseCodes; + private final Map customRetryForStatusCodes; public static abstract class Builder> { protected abstract T self(); @@ -70,7 +67,7 @@ public static abstract class Builder> { private int batchSize = 50; // Optional private int bufferCapacity = Integer.MAX_VALUE; private EventStore eventStore = null; // Optional - private List fatalResponseCodes = null; // Optional + private Map customRetryForStatusCodes = null; // Optional private int threadCount = 50; // Optional private CookieJar cookieJar = null; // Optional private ScheduledExecutorService requestExecutorService = null; // Optional @@ -134,15 +131,14 @@ public T eventStore(final EventStore eventStore) { } /** - * Provide a denylist of HTTP response codes. Retry will not be attempted if one of these codes - * is received. The events in the request will be dropped, but the Emitter will continue trying - * to send as normal. - * - * @param fatalResponseCodes Event sending will not be retried on these codes + * Set custom retry rules for HTTP status codes received in emit responses from the Collector. + * By default, retry will not occur for status codes 400, 401, 403, 410 or 422. This can be overridden here. + * Note that 2xx codes will never retry as they are considered successful. + * @param customRetryForStatusCodes Mapping of integers (status codes) to booleans (true for retry and false for not retry) * @return itself */ - public T fatalResponseCodes(final List fatalResponseCodes) { - this.fatalResponseCodes = fatalResponseCodes; + public T customRetryForStatusCodes(Map customRetryForStatusCodes) { + this.customRetryForStatusCodes = customRetryForStatusCodes; return self(); } @@ -241,10 +237,10 @@ protected BatchEmitter(final Builder builder) { eventStore = new InMemoryEventStore(builder.bufferCapacity); } - if (builder.fatalResponseCodes != null) { - fatalResponseCodes = builder.fatalResponseCodes; + if (builder.customRetryForStatusCodes != null) { + customRetryForStatusCodes = builder.customRetryForStatusCodes; } else { - fatalResponseCodes = new ArrayList<>(); + customRetryForStatusCodes = new HashMap<>(); } if (builder.requestExecutorService != null) { @@ -336,6 +332,22 @@ protected boolean isSuccessfulSend(final int code) { return code >= 200 && code < 300; } + protected boolean shouldRetry(int code) { + // don't retry if successful + if (isSuccessfulSend(code)) { + return false; + } + + // status code has a custom retry rule + if (customRetryForStatusCodes.containsKey(code)) { + return Objects.requireNonNull(customRetryForStatusCodes.get(code)); + } + + // retry if status code is not in the list of no-retry status codes + Set dontRetryStatusCodes = new HashSet<>(Arrays.asList(400, 401, 403, 410, 422)); + return !dontRetryStatusCodes.contains(code); + } + /** * Returns a Runnable POST Request operation * @@ -362,7 +374,7 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { retryDelay.set(0); eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); - } else if (fatalResponseCodes.contains(code)) { + } else if (!shouldRetry(code)) { LOGGER.debug("BatchEmitter failed to send {} events. No retry for code {}: events dropped", eventsInRequest.size(), code); eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 0095d05b..49bc6647 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -12,10 +12,7 @@ */ package com.snowplowanalytics.snowplow.tracker.emitter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.regex.Pattern; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; @@ -331,16 +328,12 @@ public void successfulSendAfterFailureResetsBackoffTime() throws InterruptedExce } @Test - public void noRetryAfterDenylistResponseCode() throws InterruptedException { - List noRetry = new ArrayList<>(); - noRetry.add(403); - + public void noRetryAfterDefaultNoRetryResponseCode() throws InterruptedException { // the FailingHttpClientAdapter always returns 403 FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); BatchEmitter emitter = BatchEmitter.builder() .httpClientAdapter(failingHttpClientAdapter) .batchSize(2) - .fatalResponseCodes(noRetry) .build(); List payloads = createPayloads(4); @@ -355,6 +348,54 @@ public void noRetryAfterDenylistResponseCode() throws InterruptedException { Assert.assertEquals(0, emitter.getBuffer().size()); } + @Test + public void retryWithCustomRulesOverridingDefault() throws InterruptedException { + Map customRetry = new HashMap<>(); + customRetry.put(403, true); + + // the FailingHttpClientAdapter always returns 403, which by default isn't retried + FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(failingHttpClientAdapter) + .customRetryForStatusCodes(customRetry) + .batchSize(2) + .build(); + + List payloads = createPayloads(4); + for (TrackerPayload payload : payloads) { + emitter.add(payload); + } + + Thread.sleep(500); + Assert.assertNotEquals(0, emitter.getRetryDelay()); + Assert.assertEquals(4, emitter.getBuffer().size()); + } + + @Test + public void noRetryWithCustomRulesOverridingDefault() throws InterruptedException { + Map customRetry = new HashMap<>(); + customRetry.put(500, false); + + // the FlakyHttpClientAdapter returns 500 for the first 4 requests + // by default, requests with code 500 are retried + FlakyHttpClientAdapter flakyHttpClientAdapter = new FlakyHttpClientAdapter(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(flakyHttpClientAdapter) + .customRetryForStatusCodes(customRetry) + .batchSize(2) + .build(); + + List payloads = createPayloads(4); + for (TrackerPayload payload : payloads) { + emitter.add(payload); + } + + Thread.sleep(500); + + Assert.assertEquals(0, emitter.getRetryDelay()); + Assert.assertEquals(0, emitter.getBuffer().size()); + } + private TrackerPayload createPayload() { PageView pv = PageView.builder() .pageUrl("https://www.snowplowanalytics.com/") From 39c59ebfcd1cbdef1c7b041a4919f93b381e3ed0 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 17 Aug 2022 14:04:45 +0100 Subject: [PATCH 105/128] Reduce the default maximum event buffer capacity (close #352) PR #353 * Set the default buffer size to 10000 * Update docstring --- .../snowplow/tracker/emitter/BatchEmitter.java | 6 +++--- .../snowplow/tracker/emitter/InMemoryEventStore.java | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 3c532aa2..c7fb390b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -65,7 +65,7 @@ public static abstract class Builder> { private HttpClientAdapter httpClientAdapter; // Optional private String collectorUrl = null; // Required if not specifying a httpClientAdapter private int batchSize = 50; // Optional - private int bufferCapacity = Integer.MAX_VALUE; + private int bufferCapacity = 10000; private EventStore eventStore = null; // Optional private Map customRetryForStatusCodes = null; // Optional private int threadCount = 50; // Optional @@ -108,8 +108,8 @@ public T batchSize(final int batchSize) { } /** - * The default buffer capacity is Integer.MAX_VALUE. Your application would likely run out - * of memory before buffering this many events. When the buffer is full, new events are lost. + * The default buffer capacity is 10 000 events. + * When the buffer is full (due to network outage), new events are lost. * * @param bufferCapacity The maximum capacity of the default InMemoryEventStore event buffer * @return itself diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index a698a16b..8c1f76ca 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -34,20 +34,21 @@ */ public class InMemoryEventStore implements EventStore { private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryEventStore.class); + private static final int DEFAULT_BUFFER_SIZE = 10000; private final AtomicLong batchId = new AtomicLong(1); private final LinkedBlockingDeque eventBuffer; private final ConcurrentHashMap> eventsBeingSent = new ConcurrentHashMap<>(); /** - * Make a new InMemoryEventStore with default queue capacity (Integer.MAX_VALUE). + * Make a new InMemoryEventStore with default queue capacity (10 000 events). */ public InMemoryEventStore() { - eventBuffer = new LinkedBlockingDeque<>(); + this(DEFAULT_BUFFER_SIZE); } /** - * Make a new InMemoryEventStore with user-set queue capacity. + * Make a new InMemoryEventStore with user-set queue capacity. The default is 10 000 events. * * @param bufferCapacity the maximum number of events to buffer at once */ From 0a77842f88041d1f01f1ea29ff384ef961e8ade2 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 17 Aug 2022 14:40:47 +0100 Subject: [PATCH 106/128] Restore Emitter callbacks for success and failure (close #339) PR #348 * Add EmitterCallback interface Test for callbacks being called * Add EmitterCallback to InMemoryEventStore Add Builder to InMemoryEventStore Set up constructors for InMemoryEventStore * Start moving EmitterCallback out of InMemoryEventStore * Remove EmitterCallback from InMemoryEventStore * Make FailureTypes more specific * Fix merge commit --- .../tracker/emitter/BatchEmitter.java | 55 +++- .../tracker/emitter/EmitterCallback.java | 28 ++ .../snowplow/tracker/emitter/EventStore.java | 2 +- .../snowplow/tracker/emitter/FailureType.java | 44 +++ .../tracker/emitter/InMemoryEventStore.java | 22 +- .../tracker/emitter/BatchEmitterTest.java | 286 ++++++++++++++---- .../emitter/InMemoryEventStoreTest.java | 3 - 7 files changed, 371 insertions(+), 69 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index c7fb390b..60f4bc1f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -58,6 +58,7 @@ public class BatchEmitter implements Emitter, Closeable { private final ScheduledExecutorService executor; private final EventStore eventStore; private final Map customRetryForStatusCodes; + private final EmitterCallback callback; public static abstract class Builder> { protected abstract T self(); @@ -71,6 +72,7 @@ public static abstract class Builder> { private int threadCount = 50; // Optional private CookieJar cookieJar = null; // Optional private ScheduledExecutorService requestExecutorService = null; // Optional + private EmitterCallback callback = null; // Optional /** * Adds a custom HttpClientAdapter to the Emitter (default is OkHttpClientAdapter). @@ -179,6 +181,17 @@ public T cookieJar(final CookieJar cookieJar) { return self(); } + /** + * Provide a custom EmitterCallback to access successfully sent or failed event payloads. + * + * @param callback an EmitterCallback + * @return itself + */ + public T callback(final EmitterCallback callback) { + this.callback = callback; + return self(); + } + public BatchEmitter build() { return new BatchEmitter(this); } @@ -231,6 +244,17 @@ protected BatchEmitter(final Builder builder) { retryDelay = new AtomicInteger(0); batchSize = builder.batchSize; + if (builder.callback != null) { + callback = builder.callback; + } else { + callback = new EmitterCallback() { + @Override + public void onSuccess(List payloads) {} + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) {} + }; + } + if (builder.eventStore != null) { eventStore = builder.eventStore; } else { @@ -248,6 +272,7 @@ protected BatchEmitter(final Builder builder) { } else { executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); } + } /** @@ -272,6 +297,7 @@ public boolean add(final TrackerPayload payload) { if (!result) { LOGGER.error("Unable to add payload to emitter, emitter buffer is full"); + callback.onFailure(FailureType.TRACKER_STORAGE_FULL, false, Collections.singletonList(payload)); } return result; @@ -357,6 +383,11 @@ protected boolean shouldRetry(int code) { private Runnable getPostRequestRunnable(int numberOfEvents) { return () -> { BatchPayload batchedEvents = null; + + // If the InMemoryEventStore queue is full when events are returned for retry, + // newer events are removed to make space + List eventsDeletedFromStorage = new ArrayList<>(); + try { batchedEvents = eventStore.getEventsBatch(numberOfEvents); @@ -364,7 +395,7 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { return; } - List eventsInRequest = batchedEvents.getPayloads(); + List eventsInRequest = new ArrayList<>(batchedEvents.getPayloads()); final SelfDescribingJson post = getFinalPost(eventsInRequest); final int code = httpClientAdapter.post(post); @@ -373,14 +404,27 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { LOGGER.debug("BatchEmitter successfully sent {} events: code: {}", eventsInRequest.size(), code); retryDelay.set(0); eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + callback.onSuccess(eventsInRequest); + } else if (!shouldRetry(code)) { LOGGER.debug("BatchEmitter failed to send {} events. No retry for code {}: events dropped", eventsInRequest.size(), code); eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); + callback.onFailure(FailureType.REJECTED_BY_COLLECTOR, false, eventsInRequest); } else { LOGGER.error("BatchEmitter failed to send {} events: code: {}", eventsInRequest.size(), code); - eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); + eventsDeletedFromStorage = eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); + + if (code == -1) { + callback.onFailure(FailureType.HTTP_CONNECTION_FAILURE, true, eventsInRequest); + } else { + callback.onFailure(FailureType.REJECTED_BY_COLLECTOR, true, eventsInRequest); + } + + if (!eventsDeletedFromStorage.isEmpty()) { + callback.onFailure(FailureType.TRACKER_STORAGE_FULL, false, eventsDeletedFromStorage); + } // exponentially increase retry backoff time after the first failure, up to the maximum wait time if (!retryDelay.compareAndSet(0, 100)) { @@ -390,7 +434,12 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { } catch (Exception e) { LOGGER.error("BatchEmitter event sending error: {}", e.getMessage()); if (batchedEvents != null) { - eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); + eventsDeletedFromStorage = eventStore.cleanupAfterSendingAttempt(true, batchedEvents.getBatchId()); + callback.onFailure(FailureType.EMITTER_REQUEST_FAILURE, true, new ArrayList<>(batchedEvents.getPayloads())); + + if (!eventsDeletedFromStorage.isEmpty()) { + callback.onFailure(FailureType.TRACKER_STORAGE_FULL, false, eventsDeletedFromStorage); + } } } }; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java new file mode 100644 index 00000000..a79ecc68 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.emitter; + +import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; + +import java.util.List; + +/** + * This interface allows the user to provide callbacks for when events are + * successfully sent to the event collector, or at other times when data loss + * may occur, specified using the FailureType enum. + */ +public interface EmitterCallback { + void onSuccess(List payloads); + + void onFailure(FailureType failureType, boolean willRetry, List payloads); +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index 3fa80d13..eca2fa83 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -50,7 +50,7 @@ public interface EventStore { * @param needRetry if another attempt should be made to send the events * @param batchId the ID of the batch of events */ - void cleanupAfterSendingAttempt(boolean needRetry, long batchId); + List cleanupAfterSendingAttempt(boolean needRetry, long batchId); /** * Get the current size of the buffer. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java new file mode 100644 index 00000000..487c25f5 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.emitter; + +/** + * The supported failure options for EmitterCallback. + */ +public enum FailureType { + /** + * A request status code other than 2xx is received. Payloads in the request + * may be automatically retried or not following this kind of failure, depending on the status code + * and the BatchEmitter configuration. + */ + REJECTED_BY_COLLECTOR, + + /** + * The InMemoryEventStore buffer is full. This could occur if the network connection + * to the event collector is down, causing payloads to accumulate in the buffer. + * This failure can occur either when the Tracker attempts to add new events to the BatchEmitter, + * or when events that need to be retried are returned to the buffer, removing newer events + * to make space if necessary. + */ + TRACKER_STORAGE_FULL, + + /** + * An exception or unsuccessful POST request in OkHttpClientAdapter. + */ + HTTP_CONNECTION_FAILURE, + + /** + * An exception during POST request in BatchEmitter. + */ + EMITTER_REQUEST_FAILURE +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index 8c1f76ca..b53a711d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -41,19 +41,18 @@ public class InMemoryEventStore implements EventStore { private final ConcurrentHashMap> eventsBeingSent = new ConcurrentHashMap<>(); /** - * Make a new InMemoryEventStore with default queue capacity (10 000 events). + * Create a InMemoryEventStore object with custom queue capacity. The default is 10 000 events. + * @param bufferCapacity the maximum number of events to buffer at once */ - public InMemoryEventStore() { - this(DEFAULT_BUFFER_SIZE); + public InMemoryEventStore(int bufferCapacity) { + eventBuffer = new LinkedBlockingDeque<>(bufferCapacity); } /** - * Make a new InMemoryEventStore with user-set queue capacity. The default is 10 000 events. - * - * @param bufferCapacity the maximum number of events to buffer at once + * Create a InMemoryEventStore object with default buffer size (10 000 events). */ - public InMemoryEventStore(int bufferCapacity) { - eventBuffer = new LinkedBlockingDeque<>(bufferCapacity); + public InMemoryEventStore() { + this(DEFAULT_BUFFER_SIZE); } /** @@ -100,11 +99,13 @@ public BatchPayload getEventsBatch(int numberToGet) { * * @param needRetry if true, move events back to the buffer instead of deleting * @param batchId the ID of the batch of events + * @return newer TrackerPayloads deleted from the queue to make space for older payloads */ @Override - public void cleanupAfterSendingAttempt(boolean needRetry, long batchId) { + public List cleanupAfterSendingAttempt(boolean needRetry, long batchId) { // Events that successfully sent are deleted from the pending buffer List events = eventsBeingSent.remove(batchId); + List removedEvents = new ArrayList<>(); // Events that didn't send are inserted at the head of the eventBuffer // for immediate resending. @@ -114,11 +115,12 @@ public void cleanupAfterSendingAttempt(boolean needRetry, long batchId) { boolean result = eventBuffer.offerFirst(payloadToReinsert); if (!result) { LOGGER.error("Event buffer is full. Dropping newer payload to reinsert older payload"); - eventBuffer.removeLast(); + removedEvents.add(eventBuffer.removeLast()); eventBuffer.offerFirst(payloadToReinsert); } } } + return removedEvents; } /** diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 49bc6647..ba00a57a 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -28,22 +28,26 @@ public class BatchEmitterTest { private MockHttpClientAdapter mockHttpClientAdapter; - private FlakyHttpClientAdapter flakyHttpClientAdapter; private BatchEmitter emitter; // MockHttpClientAdapter always returns 200 public static class MockHttpClientAdapter implements HttpClientAdapter { + private final int statusCode; public boolean isGetCalled = false; public boolean isPostCalled = false; public int postCounter = 0; public SelfDescribingJson capturedPayload; + public MockHttpClientAdapter(int statusCode) { + this.statusCode = statusCode; + } + @Override public int post(SelfDescribingJson payload) { isPostCalled = true; postCounter++; capturedPayload = payload; - return 200; + return statusCode; } @Override @@ -59,7 +63,7 @@ public int get(TrackerPayload payload) { public Object getHttpClient() { return null; } } - // this class fails to "send" the first 4 requests + // this class fails to "send" the first 4 requests (status code 500) // but returns a successful result (200) subsequently static class FlakyHttpClientAdapter implements HttpClientAdapter { int failedPostCounter = 0; @@ -85,29 +89,9 @@ public int post(SelfDescribingJson payload) { public Object getHttpClient() { return null; } } - // This class always returns failure code 403 - static class FailingHttpClientAdapter implements HttpClientAdapter { - int failedPostCounter = 0; - @Override - public int post(SelfDescribingJson payload) { - failedPostCounter++; - return 403; - } - - @Override - public int get(TrackerPayload payload) { return 0; } - - @Override - public String getUrl() { return null; } - - @Override - public Object getHttpClient() { return null; } - } - @Before public void setUp() { - mockHttpClientAdapter = new MockHttpClientAdapter(); - flakyHttpClientAdapter = new FlakyHttpClientAdapter(); + mockHttpClientAdapter = new MockHttpClientAdapter(200); emitter = BatchEmitter.builder() .httpClientAdapter(mockHttpClientAdapter) .batchSize(10) @@ -288,7 +272,7 @@ public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedExc @Test public void eventSendingFailureIncreasesBackoffTime() throws InterruptedException { emitter = BatchEmitter.builder() - .httpClientAdapter(flakyHttpClientAdapter) + .httpClientAdapter(new MockHttpClientAdapter(500)) .batchSize(1) .build(); @@ -327,36 +311,14 @@ public void successfulSendAfterFailureResetsBackoffTime() throws InterruptedExce Assert.assertEquals(0, emitter.getRetryDelay()); } - @Test - public void noRetryAfterDefaultNoRetryResponseCode() throws InterruptedException { - // the FailingHttpClientAdapter always returns 403 - FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); - BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(failingHttpClientAdapter) - .batchSize(2) - .build(); - - List payloads = createPayloads(4); - for (TrackerPayload payload : payloads) { - emitter.add(payload); - } - - Thread.sleep(500); - - Assert.assertEquals(2, failingHttpClientAdapter.failedPostCounter); - Assert.assertEquals(0, emitter.getRetryDelay()); - Assert.assertEquals(0, emitter.getBuffer().size()); - } - @Test public void retryWithCustomRulesOverridingDefault() throws InterruptedException { Map customRetry = new HashMap<>(); customRetry.put(403, true); - // the FailingHttpClientAdapter always returns 403, which by default isn't retried - FailingHttpClientAdapter failingHttpClientAdapter = new FailingHttpClientAdapter(); + // by default 403 isn't retried BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(failingHttpClientAdapter) + .httpClientAdapter(new MockHttpClientAdapter(403)) .customRetryForStatusCodes(customRetry) .batchSize(2) .build(); @@ -376,11 +338,9 @@ public void noRetryWithCustomRulesOverridingDefault() throws InterruptedExceptio Map customRetry = new HashMap<>(); customRetry.put(500, false); - // the FlakyHttpClientAdapter returns 500 for the first 4 requests // by default, requests with code 500 are retried - FlakyHttpClientAdapter flakyHttpClientAdapter = new FlakyHttpClientAdapter(); BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(flakyHttpClientAdapter) + .httpClientAdapter(new MockHttpClientAdapter(500)) .customRetryForStatusCodes(customRetry) .batchSize(2) .build(); @@ -396,6 +356,228 @@ public void noRetryWithCustomRulesOverridingDefault() throws InterruptedExceptio Assert.assertEquals(0, emitter.getBuffer().size()); } + @Test + public void callsSuccessCallbackAfterSending() throws InterruptedException { + class TestCallback implements EmitterCallback { + List payloads; + final List failureTypes = new ArrayList<>(); + + @Override + public void onSuccess(List payloads) { + this.payloads = payloads; + } + + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) { + failureTypes.add(failureType); + } + }; + + TestCallback callback = new TestCallback(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(new MockHttpClientAdapter(200)) + .batchSize(1) + .callback(callback) + .build(); + + TrackerPayload payload = createPayload(); + emitter.add(payload); + Thread.sleep(500); + + Assert.assertEquals(callback.payloads.get(0), payload); + Assert.assertTrue(callback.failureTypes.isEmpty()); + } + + @Test + public void callsFailureCallbackWhenRejectedNeedsRetry() throws InterruptedException { + class TestCallback implements EmitterCallback { + boolean willRetry; + List payloads; + final List failureTypes = new ArrayList<>(); + + @Override + public void onSuccess(List payloads) { + } + + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) { + failureTypes.add(failureType); + this.willRetry = willRetry; + this.payloads = payloads; + } + }; + + TestCallback callback = new TestCallback(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(new MockHttpClientAdapter(500)) + .batchSize(1) + .callback(callback) + .build(); + + TrackerPayload payload = createPayload(); + emitter.add(payload); + Thread.sleep(500); + + Assert.assertEquals(FailureType.REJECTED_BY_COLLECTOR, callback.failureTypes.get(0)); + Assert.assertTrue(callback.willRetry); + Assert.assertEquals(callback.payloads.get(0), payload); + } + + @Test + public void callsFailureCallbackWhenRejectedNoRetry() throws InterruptedException { + class TestCallback implements EmitterCallback { + boolean willRetry; + List payloads; + final List failureTypes = new ArrayList<>(); + + @Override + public void onSuccess(List payloads) { + } + + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) { + failureTypes.add(failureType); + this.willRetry = willRetry; + this.payloads = payloads; + } + }; + + TestCallback callback = new TestCallback(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(new MockHttpClientAdapter(403)) + .batchSize(1) + .callback(callback) + .build(); + + TrackerPayload payload = createPayload(); + emitter.add(payload); + Thread.sleep(500); + + Assert.assertEquals(FailureType.REJECTED_BY_COLLECTOR, callback.failureTypes.get(0)); + Assert.assertEquals(1, callback.failureTypes.size()); + Assert.assertFalse(callback.willRetry); + Assert.assertEquals(callback.payloads.get(0), payload); + } + + @Test + public void callsFailureCallbackIfStorageIsFull() throws InterruptedException { + class TestCallback implements EmitterCallback { + boolean willRetry; + List payloads; + final List failureTypes = new ArrayList<>(); + + @Override + public void onSuccess(List payloads) { + } + + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) { + failureTypes.add(failureType); + this.willRetry = willRetry; + this.payloads = payloads; + } + }; + + TestCallback callback = new TestCallback(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(mockHttpClientAdapter) + .bufferCapacity(1) + .callback(callback) + .build(); + + emitter.add(createPayload()); + TrackerPayload payload = createPayload(); + emitter.add(payload); + Thread.sleep(500); + + Assert.assertEquals(FailureType.TRACKER_STORAGE_FULL, callback.failureTypes.get(0)); + Assert.assertEquals(1, callback.failureTypes.size()); + Assert.assertFalse(callback.willRetry); + Assert.assertEquals(callback.payloads.get(0), payload); + } + + @Test + public void callsFailureCallbackIfHttpClientAdapterFailsToSend() throws InterruptedException { + class TestCallback implements EmitterCallback { + boolean willRetry; + List payloads; + final List failureTypes = new ArrayList<>(); + + @Override + public void onSuccess(List payloads) { + } + + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) { + failureTypes.add(failureType); + this.willRetry = willRetry; + this.payloads = payloads; + } + }; + + TestCallback callback = new TestCallback(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(new MockHttpClientAdapter(-1)) + .bufferCapacity(1) + .callback(callback) + .build(); + + TrackerPayload payload = createPayload(); + emitter.add(payload); + emitter.flushBuffer(); + Thread.sleep(500); + + Assert.assertEquals(FailureType.HTTP_CONNECTION_FAILURE, callback.failureTypes.get(0)); + Assert.assertEquals(1, callback.failureTypes.size()); + Assert.assertTrue(callback.willRetry); + Assert.assertEquals(callback.payloads.get(0), payload); + } + + @Test + public void callsFailureCallbackIfStorageIsFullWhenReturningEventsForRetry() throws InterruptedException { + class TestCallback implements EmitterCallback { + boolean willRetry; + List payloads; + final List failureTypes = new ArrayList<>(); + + @Override + public void onSuccess(List payloads) { + } + + @Override + public void onFailure(FailureType failureType, boolean willRetry, List payloads) { + failureTypes.add(failureType); + this.willRetry = willRetry; + this.payloads = payloads; + } + }; + + TestCallback callback = new TestCallback(); + BatchEmitter emitter = BatchEmitter.builder() + .httpClientAdapter(new MockHttpClientAdapter(500)) + .bufferCapacity(2) + .callback(callback) + .build(); + + TrackerPayload payload1 = createPayload(); + TrackerPayload payload2 = createPayload(); + TrackerPayload payload3 = createPayload(); + + emitter.add(payload1); + emitter.flushBuffer(); + Thread.sleep(10); + + emitter.add(payload2); + emitter.add(payload3); + Thread.sleep(500); + + Assert.assertEquals(FailureType.REJECTED_BY_COLLECTOR, callback.failureTypes.get(0)); + Assert.assertEquals(FailureType.TRACKER_STORAGE_FULL, callback.failureTypes.get(1)); + Assert.assertEquals(2, callback.failureTypes.size()); + Assert.assertFalse(callback.willRetry); + Assert.assertEquals(callback.payloads.get(0), payload3); + } + private TrackerPayload createPayload() { PageView pv = PageView.builder() .pageUrl("https://www.snowplowanalytics.com/") diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java index 214a94d2..dc499b78 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -18,8 +18,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.List; - public class InMemoryEventStoreTest { private TrackerPayload trackerPayload; @@ -109,7 +107,6 @@ public void dropNewerEventsOnFailureWhenBufferFull() { eventStore.cleanupAfterSendingAttempt(true, 1L); Assert.assertEquals(3, eventStore.size()); Assert.assertTrue(eventStore.getAllEvents().contains(differentPayload)); - } private TrackerPayload createTrackerPayload() { From bc1f94e1016df557782d843f315a67451b943766 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 24 Aug 2022 15:43:07 +0100 Subject: [PATCH 107/128] Add a Snowplow interface with the ability to initialize and manage multiple trackers (close #340) PR #354 * Start implementing Snowplow static class Add method to reset Snowplow class * Create BatchEmitter from config objects * Create Tracker from config Create Tracker from configs using Snowplow class * Fix some merge conflicts * Use null for Subject by default * Add docstrings * Add Subject test * Document Snowplow class * Add docstrings to Configs * Get just a list of namespaces from Snowplow * Tidy Snowplow createTracker() methods * Remove config setters * Add docs for config getters * Remove unopinionated NetworkConfig constructor * Improve argument order for createTracker() * Don't add a Subject by default * Handle a null SubjectConfig --- .../snowplow/tracker/Snowplow.java | 215 ++++++++++++++ .../snowplow/tracker/Subject.java | 44 ++- .../snowplow/tracker/Tracker.java | 48 +++- .../configuration/EmitterConfiguration.java | 194 +++++++++++++ .../configuration/NetworkConfiguration.java | 109 +++++++ .../configuration/SubjectConfiguration.java | 270 ++++++++++++++++++ .../configuration/TrackerConfiguration.java | 97 +++++++ .../tracker/emitter/BatchEmitter.java | 74 +++-- .../snowplow/tracker/SnowplowTest.java | 144 ++++++++++ .../snowplow/tracker/SubjectTest.java | 25 ++ .../snowplow/tracker/TrackerTest.java | 15 + .../tracker/emitter/BatchEmitterTest.java | 12 +- 12 files changed, 1193 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/EmitterConfiguration.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/SubjectConfiguration.java create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/TrackerConfiguration.java create mode 100644 src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java new file mode 100644 index 00000000..e3282d1c --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker; + +import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.SubjectConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Snowplow { + + private static final Map trackers = new HashMap<>(); + private static Tracker defaultTracker; + + /** + * @return the stored tracker namespaces + */ + public static Set getInstancedTrackerNamespaces() { + return trackers.keySet(); + } + + /** + * @return the default Tracker, or null if no default is set + */ + public static Tracker getDefaultTracker() { + return defaultTracker; + } + + /** + * Set a specific Tracker instance as the default tracker. The Tracker will be added to the Snowplow class if its + * namespace is not already stored. + * + * The first Tracker created using createTracker() or registered with registerTracker() + * will automatically be the default tracker; this method is intended for use + * with multiple trackers. + * + * @param tracker the tracker to use as default + */ + public static void setDefaultTracker(Tracker tracker) { + if (!trackers.containsKey(tracker.getNamespace())) { + Snowplow.registerTracker(tracker); + } + + defaultTracker = tracker; + } + + /** + * Set a registered Tracker as the default tracker, using its namespace. + * + * @param namespace the namespace of the tracker to set as default + * @return true if the tracker was found and set + */ + public static boolean setDefaultTracker(String namespace) { + if (trackers.containsKey(namespace)) { + defaultTracker = trackers.get(namespace); + return true; + } + + return false; + } + + /** + * Create a Snowplow tracker using Configuration objects. + * + * @param trackerConfig a TrackerConfiguration + * @param networkConfig a NetworkConfiguration (will be used to create an OkHttpClientAdapter) + * @param emitterConfig an EmitterConfiguration (will be used to create a BatchEmitter) + * @param subjectConfig a SubjectConfiguration + * @return the created Tracker + */ + public static Tracker createTracker(TrackerConfiguration trackerConfig, + NetworkConfiguration networkConfig, + EmitterConfiguration emitterConfig, + SubjectConfiguration subjectConfig) { + Subject subject = null; + if (subjectConfig != null) { + subject = new Subject(subjectConfig); + } + + BatchEmitter emitter = new BatchEmitter(networkConfig, emitterConfig); + Tracker tracker = new Tracker(trackerConfig, emitter, subject); + registerTracker(tracker); + return tracker; + } + + /** + * Create a Snowplow tracker with default configuration by providing three parameters. + * + * @param namespace unique identifier for the Tracker instance + * @param collectorUrl collector endpoint + * @param appId application ID + * @return the created Tracker + */ + public static Tracker createTracker(String namespace, String collectorUrl, String appId) { + TrackerConfiguration trackerConfig = new TrackerConfiguration(namespace, appId); + NetworkConfiguration networkConfig = new NetworkConfiguration(collectorUrl); + + return createTracker(trackerConfig, networkConfig, new EmitterConfiguration(), null); + } + + /** + * Create a Snowplow tracker using Configuration objects. + * + * @param trackerConfig a TrackerConfiguration + * @param networkConfig a NetworkConfiguration + * @param emitterConfig an EmitterConfiguration + * @return the created Tracker + */ + public static Tracker createTracker(TrackerConfiguration trackerConfig, + NetworkConfiguration networkConfig, + EmitterConfiguration emitterConfig) { + return createTracker(trackerConfig, networkConfig, emitterConfig, null); + } + + /** + * Create a Snowplow tracker using Configuration objects. + * + * @param trackerConfig a TrackerConfiguration + * @param networkConfig a NetworkConfiguration + * @return the created Tracker + */ + public static Tracker createTracker(TrackerConfiguration trackerConfig, + NetworkConfiguration networkConfig) { + return createTracker(trackerConfig, networkConfig, new EmitterConfiguration(), null); + } + + /** + * Create a Snowplow tracker using Configuration objects. + * + * @param trackerConfig a TrackerConfiguration + * @param networkConfig a NetworkConfiguration + * @param subjectConfig a SubjectConfiguration + * @return the created Tracker + */ + public static Tracker createTracker(TrackerConfiguration trackerConfig, + NetworkConfiguration networkConfig, + SubjectConfiguration subjectConfig) { + return createTracker(trackerConfig, networkConfig, new EmitterConfiguration(), subjectConfig); + } + + /** + * Register a Tracker instance that was created manually, not via the Snowplow.createTracker() method. + * + * @param tracker a Tracker instance + */ + public static void registerTracker(Tracker tracker) { + String namespace = tracker.getNamespace(); + if (trackers.containsKey(namespace)) { + throw new IllegalArgumentException("Tracker with this namespace already exists."); + } + + trackers.put(namespace, tracker); + + if (defaultTracker == null) { + defaultTracker = tracker; + } + } + + /** + * Get a Tracker by its namespace + * + * @param namespace the namespace of the tracker to retrieve + * @return the retrieved tracker + */ + public static Tracker getTracker(String namespace) { + return trackers.get(namespace); + } + + /** + * Unregister a Tracker, using its namespace. + * + * @param namespace the namespace of the tracker to remove + * @return true if the tracker was found and removed + */ + public static boolean removeTracker(String namespace) { + Tracker removedTracker = trackers.remove(namespace); + if ((defaultTracker != null) && defaultTracker.getNamespace().equals(namespace)) { + defaultTracker = null; + } + return removedTracker != null; + } + + /** + * Unregister a Tracker. + * + * @param tracker the tracker to remove + * @return true if the tracker was found and removed + */ + public static boolean removeTracker(Tracker tracker) { + return removeTracker(tracker.getNamespace()); + } + + /** + * Clear (unregister) all trackers. + */ + public static void reset() { + trackers.clear(); + defaultTracker = null; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index fd298035..14a7b04a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -17,6 +17,7 @@ import java.util.Map; // This library +import com.snowplowanalytics.snowplow.tracker.configuration.SubjectConfiguration; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; /** @@ -29,22 +30,22 @@ public class Subject { private HashMap standardPairs = new HashMap<>(); /** - * Creates a Subject which will add extra data to each event. + * Creates a Subject instance from a SubjectConfiguration. * - * @param builder The builder that constructs a subject + * @param subjectConfig a SubjectConfiguration */ - private Subject(SubjectBuilder builder) { - this.setUserId(builder.userId); - this.setScreenResolution(builder.screenResWidth, builder.screenResHeight); - this.setViewPort(builder.viewPortWidth, builder.viewPortHeight); - this.setColorDepth(builder.colorDepth); - this.setTimezone(builder.timezone); - this.setLanguage(builder.language); - this.setIpAddress(builder.ipAddress); - this.setUseragent(builder.useragent); - this.setNetworkUserId(builder.networkUserId); - this.setDomainUserId(builder.domainUserId); - this.setDomainSessionId(builder.domainSessionId); + public Subject(SubjectConfiguration subjectConfig) { + setUserId(subjectConfig.getUserId()); + setScreenResolution(subjectConfig.getScreenResWidth(), subjectConfig.getScreenResHeight()); + setViewPort(subjectConfig.getViewPortWidth(), subjectConfig.getViewPortHeight()); + setColorDepth(subjectConfig.getColorDepth()); + setTimezone(subjectConfig.getTimezone()); + setLanguage(subjectConfig.getLanguage()); + setIpAddress(subjectConfig.getIpAddress()); + setUseragent(subjectConfig.getUseragent()); + setNetworkUserId(subjectConfig.getNetworkUserId()); + setDomainUserId(subjectConfig.getDomainUserId()); + setDomainSessionId(subjectConfig.getDomainSessionId()); } /** @@ -189,7 +190,20 @@ public SubjectBuilder domainSessionId(String domainSessionId) { * @return a new Subject object */ public Subject build() { - return new Subject(this); + SubjectConfiguration subjectConfig = new SubjectConfiguration() + .userId(userId) + .screenResolution(screenResWidth, screenResHeight) + .viewPort(viewPortWidth, viewPortHeight) + .colorDepth(colorDepth) + .timezone(timezone) + .language(language) + .ipAddress(ipAddress) + .useragent(useragent) + .networkUserId(networkUserId) + .domainUserId(domainUserId) + .domainSessionId(domainSessionId); + + return new Subject(subjectConfig); } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 72555d7a..0aad41f3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -12,6 +12,7 @@ */ package com.snowplowanalytics.snowplow.tracker; +import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.emitter.Emitter; @@ -34,29 +35,39 @@ public class Tracker { /** * Creates a new Snowplow Tracker. * - * @param builder The builder that constructs a tracker + * @param trackerConfig a TrackerConfiguration object + * @param emitter an Emitter + * */ - private Tracker(TrackerBuilder builder) { + public Tracker(TrackerConfiguration trackerConfig, Emitter emitter) { + this(trackerConfig, emitter, null); + } + + /** + * Creates a new Snowplow Tracker. + * + * @param trackerConfig a TrackerConfiguration object + * @param emitter an Emitter + * @param subject a Subject + * + */ + public Tracker(TrackerConfiguration trackerConfig, Emitter emitter, Subject subject) { // Precondition checks - Objects.requireNonNull(builder.emitter); - Objects.requireNonNull(builder.namespace); - Objects.requireNonNull(builder.appId); - if (builder.namespace.isEmpty()) { + Objects.requireNonNull(emitter); + Objects.requireNonNull(trackerConfig.getNamespace()); + Objects.requireNonNull(trackerConfig.getAppId()); + if (trackerConfig.getNamespace().isEmpty()) { throw new IllegalArgumentException("namespace cannot be empty"); } - if (builder.appId.isEmpty()) { + if (trackerConfig.getAppId().isEmpty()) { throw new IllegalArgumentException("appId cannot be empty"); } - this.parameters = new TrackerParameters(builder.appId, builder.platform, builder.namespace, Version.TRACKER, builder.base64Encoded); - this.emitter = builder.emitter; - this.subject = builder.subject; - - } + this.parameters = new TrackerParameters(trackerConfig.getAppId(), trackerConfig.getPlatform(), trackerConfig.getNamespace(), Version.TRACKER, trackerConfig.isBase64Encoded()); + this.emitter = emitter; + this.subject = subject; - public static TrackerBuilder builder(Emitter emitter, String namespace, String appId) { - return new TrackerBuilder(emitter, namespace, appId); } /** @@ -119,10 +130,17 @@ public TrackerBuilder base64(Boolean base64) { * @return a new Tracker object */ public Tracker build() { - return new Tracker(this); + TrackerConfiguration trackerConfig = new TrackerConfiguration(namespace, appId) + .platform(platform) + .base64Encoded(base64Encoded); + return new Tracker(trackerConfig, emitter, subject); } } + public static TrackerBuilder builder(Emitter emitter, String namespace, String appId) { + return new TrackerBuilder(emitter, namespace, appId); + } + // --- Setters /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/EmitterConfiguration.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/EmitterConfiguration.java new file mode 100644 index 00000000..23bed0fa --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/EmitterConfiguration.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.configuration; + +import com.snowplowanalytics.snowplow.tracker.emitter.EmitterCallback; +import com.snowplowanalytics.snowplow.tracker.emitter.EventStore; + +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; + +public class EmitterConfiguration { + + private int batchSize; // Optional + private int bufferCapacity; // Optional + private EventStore eventStore; // Optional + private Map customRetryForStatusCodes; // Optional + private int threadCount; // Optional + private ScheduledExecutorService requestExecutorService; // Optional + private EmitterCallback callback; // Optional + + // Getters and Setters + + /** + * Returns the number of events to send per request (batched). + * @return the batch size + */ + public int getBatchSize() { + return batchSize; + } + + /** + * Returns the maximum number of events to buffer in memory. + * @return maximum buffer capacity + */ + public int getBufferCapacity() { + return bufferCapacity; + } + + /** + * Returns the EventStore used to buffer events. + * @return EventStore instance + */ + public EventStore getEventStore() { + return eventStore; + } + + /** + * Returns the custom configuration for HTTP status codes. "True" means the + * @return map of integers (status codes) to booleans (true for retry and false for not retry) + */ + public Map getCustomRetryForStatusCodes() { + return customRetryForStatusCodes; + } + + /** + * Returns the number of threads used for event sending using the ScheduledExecutorService. + * @return thread count + */ + public int getThreadCount() { + return threadCount; + } + + /** + * Returns the ScheduledExecutorService used for sending events. + * @return ScheduledExecutorService object + */ + public ScheduledExecutorService getRequestExecutorService() { + return requestExecutorService; + } + + /** + * Returns the custom callback which is called when events are successfully sent to the collector, + * or after certain failure conditions. + * + * @return EmitterCallback object + */ + public EmitterCallback getCallback() { + return callback; + } + + // Constructor + + /** + * Create an EmitterConfiguration instance. The default configuration is: + * 50 batched events per request; + * maximum 10 000 events buffered in memory; + * 50 threads; + * no retry for request status codes 400, 401, 403, 410 or 422; + * and OkHttp (OkHttpClientAdapter) used for HTTP requests. + */ + public EmitterConfiguration() { + batchSize = 50; + bufferCapacity = 10000; + eventStore = null; + customRetryForStatusCodes = null; + threadCount = 50; + requestExecutorService = null; + callback = null; + } + + // Builder methods + + /** + * The default batch size is 50. + * + * @param batchSize The count of events to send in one HTTP request + * @return itself + */ + public EmitterConfiguration batchSize(int batchSize) { + this.batchSize = batchSize; + return this; + } + + /** + * The default buffer capacity is 10 000 events. + * When the buffer is full (due to network outage), new events are lost. + * + * @param bufferCapacity The maximum capacity of the default InMemoryEventStore event buffer + * @return itself + */ + public EmitterConfiguration bufferCapacity(int bufferCapacity) { + this.bufferCapacity = bufferCapacity; + return this; + } + + /** + * The default EventStore is InMemoryEventStore. + * + * @param eventStore The EventStore to use + * @return itself + */ + public EmitterConfiguration eventStore(EventStore eventStore) { + this.eventStore = eventStore; + return this; + } + + /** + * Set custom retry rules for HTTP status codes received in emit responses from the Collector. + * By default, retry will not occur for status codes 400, 401, 403, 410 or 422. This can be overridden here. + * Note that 2xx codes will never retry as they are considered successful. + * @param customRetryForStatusCodes Mapping of integers (status codes) to booleans (true for retry and false for not retry) + * @return itself + */ + public EmitterConfiguration customRetryForStatusCodes(Map customRetryForStatusCodes) { + this.customRetryForStatusCodes = customRetryForStatusCodes; + return this; + } + + /** + * Sets the Thread Count for the ScheduledExecutorService (default is 50). + * + * @param threadCount the size of the thread pool + * @return itself + */ + public EmitterConfiguration threadCount(int threadCount) { + this.threadCount = threadCount; + return this; + } + + /** + * Set a custom ScheduledExecutorService to send http requests (default is ScheduledThreadPoolExecutor). + *

+ * Implementation note: Be aware that calling `close()` on a BatchEmitter instance + * has a side-effect and will shutdown that ExecutorService. + * + * @param requestExecutorService the ScheduledExecutorService to use + * @return itself + */ + public EmitterConfiguration requestExecutorService(ScheduledExecutorService requestExecutorService) { + this.requestExecutorService = requestExecutorService; + return this; + } + + /** + * Provide a custom EmitterCallback to access successfully sent or failed event payloads. + * + * @param callback an EmitterCallback + * @return itself + */ + public EmitterConfiguration callback(EmitterCallback callback) { + this.callback = callback; + return this; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java new file mode 100644 index 00000000..c75bd159 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.configuration; + +import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; +import okhttp3.CookieJar; + + +public class NetworkConfiguration { + + private HttpClientAdapter httpClientAdapter = null; // Optional + private String collectorUrl = null; // Required if not specifying a httpClientAdapter + private CookieJar cookieJar = null; // Optional + + // Getters and Setters + + /** + * Returns the HttpClientAdapter used. + * @return HttpClientAdapter object + */ + public HttpClientAdapter getHttpClientAdapter() { + return httpClientAdapter; + } + + /** + * Returns the event collector URL endpoint. + * @return collector URL + */ + public String getCollectorUrl() { + return collectorUrl; + } + + /** + * Returns the OkHttp CookieJar used for persisting cookies. + * @return CookieJar object + */ + public CookieJar getCookieJar() { + return cookieJar; + } + + // Constructors + + /** + * Create a NetworkConfiguration instance and specify a custom HttpClientAdapter to use + * (the default is OkHttpClientAdapter). + * + * @param httpClientAdapter the adapter to use + */ + public NetworkConfiguration(HttpClientAdapter httpClientAdapter) { + this.httpClientAdapter = httpClientAdapter; + } + + /** + * Create a NetworkConfiguration instance with a collector endpoint URL. The URL will be used + * to create the default OkHttpClientAdapter. + * + * @param collectorUrl the url for the default httpClientAdapter + */ + public NetworkConfiguration(String collectorUrl) { + this.collectorUrl = collectorUrl; + } + + // Builder methods + + /** + * Sets a custom HttpClientAdapter (default is OkHttpClientAdapter). + * + * @param httpClientAdapter the adapter to use + * @return itself + */ + public NetworkConfiguration httpClientAdapter(HttpClientAdapter httpClientAdapter) { + this.httpClientAdapter = httpClientAdapter; + return this; + } + + /** + * Sets the endpoint url for when a httpClientAdapter is not specified. + * It will be used to create the default OkHttpClientAdapter. + * + * @param collectorUrl the url for the default httpClientAdapter + * @return itself + */ + public NetworkConfiguration collectorUrl(String collectorUrl) { + this.collectorUrl = collectorUrl; + return this; + } + + /** + * Adds a custom CookieJar to be used with OkHttpClientAdapters. + * Will be ignored if a custom httpClientAdapter is provided. + * + * @param cookieJar the CookieJar to use + * @return itself + */ + public NetworkConfiguration cookieJar(CookieJar cookieJar) { + this.cookieJar = cookieJar; + return this; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/SubjectConfiguration.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/SubjectConfiguration.java new file mode 100644 index 00000000..194c2468 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/SubjectConfiguration.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.configuration; + +import com.snowplowanalytics.snowplow.tracker.Utils; + +public class SubjectConfiguration { + + private String userId; // Optional + private int screenResWidth; // Optional + private int screenResHeight; // Optional + private int viewPortWidth; // Optional + private int viewPortHeight; // Optional + private int colorDepth; // Optional + private String timezone; // Optional + private String language; // Optional + private String ipAddress; // Optional + private String useragent; // Optional + private String networkUserId; // Optional + private String domainUserId; // Optional + private String domainSessionId; // Optional + + // Getters and Setters + + /** + * Returns the user ID. + * @return user ID + */ + public String getUserId() { + return userId; + } + + /** + * Returns the screen resolution width, in pixels. + * @return screen width + */ + public int getScreenResWidth() { + return screenResWidth; + } + + /** + * Returns the screen resolution height, in pixels. + * @return screen height + */ + public int getScreenResHeight() { + return screenResHeight; + } + + /** + * Returns the viewport width, in pixels. + * @return viewport width + */ + public int getViewPortWidth() { + return viewPortWidth; + } + + /** + * Returns the viewport height, in pixels. + * @return viewport height + */ + public int getViewPortHeight() { + return viewPortHeight; + } + + /** + * Returns the color depth. + * @return color depth + */ + public int getColorDepth() { + return colorDepth; + } + + /** + * Returns the timezone. Automatically set by default to that of the server. + * @return timezone + */ + public String getTimezone() { + return timezone; + } + + /** + * Returns the device language. + * @return language + */ + public String getLanguage() { + return language; + } + + /** + * Returns the IP address. + * @return IP address + */ + public String getIpAddress() { + return ipAddress; + } + + /** + * Returns the useragent. + * @return useragent + */ + public String getUseragent() { + return useragent; + } + + /** + * Returns the network user ID (UUID string). + * @return network user ID + */ + public String getNetworkUserId() { + return networkUserId; + } + + /** + * Returns the domain user ID (UUID string). + * @return domain user ID + */ + public String getDomainUserId() { + return domainUserId; + } + + /** + * Returns the domain session ID (UUID string). + * @return domain session ID + */ + public String getDomainSessionId() { + return domainSessionId; + } + + // Constructor + + /** + * Create a Subject instance. By default, timezone is set to the server's timezone. + */ + public SubjectConfiguration() { + userId = null; // Optional + screenResWidth = 0; // Optional + screenResHeight = 0; // Optional + viewPortWidth = 0; // Optional + viewPortHeight = 0; // Optional + colorDepth = 0; // Optional + timezone = Utils.getTimezone(); // Optional + language = null; // Optional + ipAddress = null; // Optional + useragent = null; // Optional + networkUserId = null; // Optional + domainUserId = null; // Optional + domainSessionId = null; // Optional + } + + // Builder methods + + /** + * Set a unique user ID. + * @param userId a user ID + * @return itself + */ + public SubjectConfiguration userId(String userId) { + this.userId = userId; + return this; + } + + /** + * Set the screen resolution. + * @param width width in pixels + * @param height height in pixels + * @return itself + */ + public SubjectConfiguration screenResolution(int width, int height) { + screenResWidth = width; + screenResHeight = height; + return this; + } + + /** + * Set the viewport size. + * @param width width in pixels + * @param height height in pixels + * @return itself + */ + public SubjectConfiguration viewPort(int width, int height) { + viewPortWidth = width; + viewPortHeight = height; + return this; + } + + /** + * @param depth a color depth integer + * @return itself + */ + public SubjectConfiguration colorDepth(int depth) { + colorDepth = depth; + return this; + } + + /** + * Note that timezone is set by default to the server's timezone + * (`TimeZone tz = Calendar.getInstance().getTimeZone().getID()`) + * @param timezone a timezone string + * @return itself + */ + public SubjectConfiguration timezone(String timezone) { + this.timezone = timezone; + return this; + } + + /** + * @param language a language string + * @return itself + */ + public SubjectConfiguration language(String language) { + this.language = language; + return this; + } + + /** + * @param ipAddress a ipAddress string + * @return itself + */ + public SubjectConfiguration ipAddress(String ipAddress) { + this.ipAddress = ipAddress; + return this; + } + + /** + * @param useragent a useragent string + * @return itself + */ + public SubjectConfiguration useragent(String useragent) { + this.useragent = useragent; + return this; + } + + /** + * This overrides the network user ID set by the Collector in response Cookies. + * @param networkUserId a networkUserId string + * @return itself + */ + public SubjectConfiguration networkUserId(String networkUserId) { + this.networkUserId = networkUserId; + return this; + } + + /** + * @param domainUserId a domainUserId string + * @return itself + */ + public SubjectConfiguration domainUserId(String domainUserId) { + this.domainUserId = domainUserId; + return this; + } + + /** + * @param domainSessionId a domainSessionId string + * @return itself + */ + public SubjectConfiguration domainSessionId(String domainSessionId) { + this.domainSessionId = domainSessionId; + return this; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/TrackerConfiguration.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/TrackerConfiguration.java new file mode 100644 index 00000000..6a11f2ba --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/TrackerConfiguration.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.configuration; + +import com.snowplowanalytics.snowplow.tracker.DevicePlatform; + + +public class TrackerConfiguration { + private final String namespace; // Required + private final String appId; // Required + private DevicePlatform platform; // Optional + private boolean base64Encoded; // Optional + + // Getters and Setters + + /** + * Returns the unique tracker namespace. + * @return tracker namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * Returns the application ID. + * @return application ID + */ + public String getAppId() { + return appId; + } + + /** + * Returns the DevicePlatform for the tracker. + * @return what platform the app is running on + */ + public DevicePlatform getPlatform() { + return platform; + } + + /** + * Returns whether JSONs in the payload are base-64 encoded. + * @return true if encoded + */ + public boolean isBase64Encoded() { + return base64Encoded; + } + + // Constructor + + /** + * Create a TrackerConfiguration instance. The namespace is the unique identifier for the instance. + * By default, the platform is ServerSideApp, and JSONs will be base64 encoded. + * + * @param namespace identifier for the Tracker instance + * @param appId application ID + */ + public TrackerConfiguration(String namespace, String appId) { + this.namespace = namespace; + this.appId = appId; + this.platform = DevicePlatform.ServerSideApp; + this.base64Encoded = true; + } + + // Builder methods + + /** + * The {@link DevicePlatform} the tracker is running on (default is "srv", ServerSideApp). + * + * @param platform The device platform the tracker is running on + * @return itself + */ + public TrackerConfiguration platform(DevicePlatform platform) { + this.platform = platform; + return this; + } + + /** + * Whether JSONs in the payload should be base-64 encoded (default is true) + * + * @param base64Encoded JSONs should be encoded or not + * @return itself + */ + public TrackerConfiguration base64Encoded(boolean base64Encoded) { + this.base64Encoded = base64Encoded; + return this; + } +} diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 60f4bc1f..5aef6073 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -20,6 +20,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; import com.snowplowanalytics.snowplow.tracker.constants.Constants; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; @@ -193,7 +195,20 @@ public T callback(final EmitterCallback callback) { } public BatchEmitter build() { - return new BatchEmitter(this); + NetworkConfiguration networkConfig = new NetworkConfiguration(collectorUrl) + .httpClientAdapter(httpClientAdapter) + .cookieJar(cookieJar); + + EmitterConfiguration emitterConfig = new EmitterConfiguration() + .batchSize(batchSize) + .bufferCapacity(bufferCapacity) + .eventStore(eventStore) + .customRetryForStatusCodes(customRetryForStatusCodes) + .threadCount(threadCount) + .requestExecutorService(requestExecutorService) + .callback(callback); + + return new BatchEmitter(networkConfig, emitterConfig); } } @@ -208,44 +223,50 @@ public static Builder builder() { return new Builder2(); } - protected BatchEmitter(final Builder builder) { + /** + * Creates a BatchEmitter object from configuration objects. + * + * @param networkConfig a NetworkConfiguration object + * @param emitterConfig an EmitterConfiguration object + */ + public BatchEmitter(NetworkConfiguration networkConfig, EmitterConfiguration emitterConfig) { OkHttpClient client; // Precondition checks - if (builder.threadCount <= 0) { + if (emitterConfig.getThreadCount() <= 0) { throw new IllegalArgumentException("threadCount must be greater than 0"); } - if (builder.batchSize <= 0) { + if (emitterConfig.getBatchSize() <= 0) { throw new IllegalArgumentException("batchSize must be greater than 0"); } - if (builder.bufferCapacity <= 0) { + if (emitterConfig.getBufferCapacity() <= 0) { throw new IllegalArgumentException("bufferCapacity must be greater than 0"); } - if (builder.httpClientAdapter != null) { - httpClientAdapter = builder.httpClientAdapter; + if (networkConfig.getHttpClientAdapter() != null) { + httpClientAdapter = networkConfig.getHttpClientAdapter(); } else { - Objects.requireNonNull(builder.collectorUrl, "Collector url must be specified if not using a httpClientAdapter"); + Objects.requireNonNull(networkConfig.getCollectorUrl(), "Collector url must be specified if not using a httpClientAdapter"); - if (builder.cookieJar != null) { + if (networkConfig.getCookieJar() != null) { client = new OkHttpClient.Builder() - .cookieJar(builder.cookieJar) + .cookieJar(networkConfig.getCookieJar()) .build(); } else { client = new OkHttpClient.Builder().build(); } httpClientAdapter = OkHttpClientAdapter.builder() // use okhttp as a default - .url(builder.collectorUrl) + .url(networkConfig.getCollectorUrl()) .httpClient(client) .build(); } retryDelay = new AtomicInteger(0); - batchSize = builder.batchSize; + batchSize = emitterConfig.getBatchSize(); - if (builder.callback != null) { - callback = builder.callback; + if (emitterConfig.getCallback() != null) { + callback = emitterConfig.getCallback(); } else { callback = new EmitterCallback() { @Override @@ -255,24 +276,32 @@ public void onFailure(FailureType failureType, boolean willRetry, List(); } - if (builder.requestExecutorService != null) { - executor = builder.requestExecutorService; + if (emitterConfig.getRequestExecutorService() != null) { + executor = emitterConfig.getRequestExecutorService(); } else { - executor = Executors.newScheduledThreadPool(builder.threadCount, new EmitterThreadFactory()); + executor = Executors.newScheduledThreadPool(emitterConfig.getThreadCount(), new EmitterThreadFactory()); } + } + /** + * Creates a BatchEmitter instance using a NetworkConfiguration. + * + * @param networkConfig a NetworkConfiguration object + */ + public BatchEmitter(NetworkConfiguration networkConfig) { + this(networkConfig, new EmitterConfiguration()); } /** @@ -406,7 +435,6 @@ private Runnable getPostRequestRunnable(int numberOfEvents) { eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); callback.onSuccess(eventsInRequest); - } else if (!shouldRetry(code)) { LOGGER.debug("BatchEmitter failed to send {} events. No retry for code {}: events dropped", eventsInRequest.size(), code); eventStore.cleanupAfterSendingAttempt(false, batchedEvents.getBatchId()); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java new file mode 100644 index 00000000..5e6446dc --- /dev/null +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker; + +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SnowplowTest { + + @After + public void cleanUp(){ + Snowplow.reset(); + } + + @Test + public void createsAndRetrievesATracker() { + assertTrue(Snowplow.getInstancedTrackerNamespaces().isEmpty()); + + Tracker tracker = Snowplow.createTracker("namespace", "http://endpoint", "appId"); + Tracker retrievedTracker = Snowplow.getTracker("namespace"); + + assertFalse(Snowplow.getInstancedTrackerNamespaces().isEmpty()); + assertEquals(tracker, retrievedTracker); + assertEquals("namespace", tracker.getNamespace()); + assertEquals("appId", tracker.getAppId()); + assertTrue(tracker.getBase64Encoded()); + } + + @Test + public void preventsDuplicateNamespaces() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + Snowplow.createTracker("namespace", "http://endpoint", "appId"); + Snowplow.createTracker("namespace", "http://collector", "appId2"); + }); + + assertEquals("Tracker with this namespace already exists.", exception.getMessage()); + } + + @Test + public void deletesStoredTracker() { + Snowplow.createTracker("namespace", "http://endpoint", "appId"); + boolean result = Snowplow.removeTracker("namespace"); + assertTrue(result); + + Tracker tracker = Snowplow.createTracker("namespace2", "http://endpoint", "appId"); + boolean result2 = Snowplow.removeTracker(tracker); + assertTrue(result2); + } + + @Test + public void doesNotDeleteUnregisteredTracker() { + BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); + Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "appId").build(); + + boolean result = Snowplow.removeTracker(tracker); + assertFalse(result); + + boolean result2 = Snowplow.removeTracker("not registered"); + assertFalse(result2); + } + + @Test + public void setsDefaultTrackerFromObject() { + assertNull(Snowplow.getDefaultTracker()); + + Tracker tracker = Snowplow.createTracker("namespace", "http://endpoint", "appId"); + assertEquals(tracker, Snowplow.getDefaultTracker()); + + Tracker tracker2 = Snowplow.createTracker("namespace2", "http://endpoint", "appId"); + // The first tracker is still the default + assertEquals(tracker, Snowplow.getDefaultTracker()); + + Snowplow.setDefaultTracker(tracker2); + assertEquals(tracker2, Snowplow.getDefaultTracker()); + assertEquals(2, Snowplow.getInstancedTrackerNamespaces().size()); + } + + @Test + public void setsDefaultTrackerFromNamespace() { + assertNull(Snowplow.getDefaultTracker()); + + Snowplow.createTracker("namespace", "http://endpoint", "appId"); + Tracker tracker2 = Snowplow.createTracker("namespace2", "http://endpoint", "appId"); + + boolean result = Snowplow.setDefaultTracker("namespace2"); + assertTrue(result); + assertEquals(tracker2, Snowplow.getDefaultTracker()); + } + + @Test + public void registersATrackerMadeWithoutSnowplowClass() { + BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); + Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "appId").build(); + + Snowplow.registerTracker(tracker); + + assertEquals(tracker, Snowplow.getDefaultTracker()); + assertEquals(1, Snowplow.getInstancedTrackerNamespaces().size()); + } + + @Test + public void settingNewDefaultTrackerRegistersIt() { + BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); + Tracker tracker = new Tracker.TrackerBuilder(emitter, "new_tracker", "appId").build(); + + Snowplow.setDefaultTracker(tracker); + + assertEquals(1, Snowplow.getInstancedTrackerNamespaces().size()); + assertEquals("new_tracker", Snowplow.getDefaultTracker().getNamespace()); + } + + @Test + public void createsTrackerFromConfigs() { + TrackerConfiguration trackerConfig = new TrackerConfiguration("namespace", "appId") + .base64Encoded(false) + .platform(DevicePlatform.Desktop); + NetworkConfiguration networkConfig = new NetworkConfiguration("http://collector-endpoint"); + + Tracker tracker = Snowplow.createTracker(trackerConfig, networkConfig); + Tracker retrievedTracker = Snowplow.getTracker("namespace"); + + assertFalse(Snowplow.getInstancedTrackerNamespaces().isEmpty()); + assertEquals(tracker, retrievedTracker); + assertEquals("namespace", tracker.getNamespace()); + assertEquals("appId", tracker.getAppId()); + assertEquals(DevicePlatform.Desktop, tracker.getPlatform()); + assertFalse(tracker.getBase64Encoded()); + } +} diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 4e865ad8..8cbff08c 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -17,6 +17,7 @@ import java.util.Map; // JUnit +import com.snowplowanalytics.snowplow.tracker.configuration.SubjectConfiguration; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -111,4 +112,28 @@ public void testGetSubject() { assertEquals(expected, subject.getSubject()); } + + @Test + public void testCreateWithBuilder() { + Subject subject = Subject.builder() + .domainSessionId("domain session ID") + .viewPort(123, 456) + .language("en") + .build(); + + assertEquals("domain session ID", subject.getSubject().get("sid")); + assertEquals("123x456", subject.getSubject().get("vp")); + assertEquals("en", subject.getSubject().get("lang")); + } + + @Test + public void testCreateFromConfig() { + SubjectConfiguration subjectConfig = new SubjectConfiguration() + .ipAddress("xxx.000.xxx.111") + .useragent("Mac OS"); + Subject subject = new Subject(subjectConfig); + + assertEquals("xxx.000.xxx.111", subject.getSubject().get("ip")); + assertEquals("Mac OS", subject.getSubject().get("ua")); + } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index ba7f544e..320d6ece 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -15,6 +15,9 @@ import java.util.*; import static java.util.Collections.singletonList; +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; +import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; @@ -561,6 +564,18 @@ public void testTrackTimingWithSubject() throws InterruptedException { // --- Tracker Setter & Getter Tests + @Test + public void testCreateWithConfiguration() { + TrackerConfiguration trackerConfig = new TrackerConfiguration("namespace", "appId"); + trackerConfig.base64Encoded(false); + trackerConfig.platform(DevicePlatform.General); + BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); + Tracker tracker = new Tracker(trackerConfig, emitter); + + assertEquals("namespace", tracker.getNamespace()); + assertEquals(emitter, tracker.getEmitter()); + } + @Test public void testGetTrackerVersion() { Tracker tracker = Tracker.builder(mockEmitter, "namespace", "an-app-id").build(); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index ba00a57a..0aeb57f3 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -15,6 +15,8 @@ import java.util.*; import java.util.regex.Pattern; +import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; import com.snowplowanalytics.snowplow.tracker.constants.Parameter; import org.junit.Assert; import org.junit.Before; @@ -30,7 +32,6 @@ public class BatchEmitterTest { private MockHttpClientAdapter mockHttpClientAdapter; private BatchEmitter emitter; - // MockHttpClientAdapter always returns 200 public static class MockHttpClientAdapter implements HttpClientAdapter { private final int statusCode; public boolean isGetCalled = false; @@ -248,6 +249,15 @@ public void close_sendsEventsAndStopsThreads() throws InterruptedException { Assert.assertEquals(20, emitter.getBuffer().size()); } + @Test + public void createEmitterWithConfiguration() { + NetworkConfiguration networkConfig = new NetworkConfiguration("http://endpoint"); + EmitterConfiguration emitterConfig = new EmitterConfiguration().batchSize(5); + BatchEmitter emitter = new BatchEmitter(networkConfig, emitterConfig); + + Assert.assertEquals(5, emitter.getBatchSize()); + } + @Test public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedException { emitter = BatchEmitter.builder() From e3e6137c114ada1da3fa319420397825e8e0870b Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 25 Aug 2022 09:59:27 +0100 Subject: [PATCH 108/128] Deprecate Builder classes (close #355) PR #356 * Deprecate SubjectBuilder, TrackerBuilder and BatchEmitterBuilder Deprecate TrackerBuilder and BatchEmitterBuilder * Create constructors for HttpClientAdapters * Update demo app * Reorder arguments in Snowplow.createTracker() * Remove unnecessary Tracker constructor * Correct mistake in BatchEmitter deprecation notice * Add @Deprecated tag * Add empty constructor for Subject * Remove unused imports --- .../main/java/com/snowplowanalytics/Main.java | 24 ++-- .../snowplow/tracker/Snowplow.java | 4 +- .../snowplow/tracker/Subject.java | 16 ++- .../snowplow/tracker/Tracker.java | 32 +++-- .../tracker/emitter/BatchEmitter.java | 10 ++ .../snowplow/tracker/emitter/EventStore.java | 1 + .../http/AbstractHttpClientAdapter.java | 19 +++ .../tracker/http/ApacheHttpClientAdapter.java | 24 ++++ .../tracker/http/OkHttpClientAdapter.java | 24 ++++ .../snowplow/tracker/SnowplowTest.java | 30 ++--- .../snowplow/tracker/SubjectTest.java | 39 +++--- .../snowplow/tracker/TrackerTest.java | 47 +++----- .../tracker/emitter/BatchEmitterTest.java | 114 +++++++----------- .../tracker/http/HttpClientAdapterTest.java | 15 +-- 14 files changed, 225 insertions(+), 174 deletions(-) diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index a0bcfaa6..9815d240 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -13,9 +13,12 @@ package com.snowplowanalytics; -import com.snowplowanalytics.snowplow.tracker.DevicePlatform; +import com.snowplowanalytics.snowplow.tracker.Snowplow; import com.snowplowanalytics.snowplow.tracker.Subject; import com.snowplowanalytics.snowplow.tracker.Tracker; +import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; +import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -42,17 +45,13 @@ public static void main(String[] args) throws InterruptedException { // the namespace to attach to events String namespace = "demo"; - // build an emitter, this is used by the tracker to batch and schedule transmission of events - BatchEmitter emitter = BatchEmitter.builder() - .url(collectorEndpoint) - .batchSize(4) // send batches of 4 events. In production this number should be higher, depending on the size/event volume - .build(); + // The easiest way to build a tracker is with configuration classes + TrackerConfiguration trackerConfig = new TrackerConfiguration(namespace, appId); + NetworkConfiguration networkConfig = new NetworkConfiguration(collectorEndpoint); + EmitterConfiguration emitterConfig = new EmitterConfiguration().batchSize(4); // send batches of 4 events. In production this number should be higher, depending on the size/event volume - // now we have the emitter, we need a tracker to turn our events into something a Snowplow collector can understand - final Tracker tracker = Tracker.builder(emitter, namespace, appId) - .base64(true) - .platform(DevicePlatform.ServerSideApp) - .build(); + // We need a tracker to turn our events into something a Snowplow collector can understand + final Tracker tracker = Snowplow.createTracker(trackerConfig, networkConfig, emitterConfig); System.out.println("Sending events to " + collectorEndpoint); System.out.println("Using tracker version " + tracker.getTrackerVersion()); @@ -64,7 +63,7 @@ public static void main(String[] args) throws InterruptedException { Collections.singletonMap("foo", "bar"))); // This is an example of a eventSubject for adding user data - Subject eventSubject = Subject.builder().build(); + Subject eventSubject = new Subject(); eventSubject.setUserId("example@snowplowanalytics.com"); eventSubject.setLanguage("EN"); @@ -152,6 +151,7 @@ public static void main(String[] args) throws InterruptedException { tracker.track(structured); // Will close all threads and force send remaining events + BatchEmitter emitter = (BatchEmitter) tracker.getEmitter(); emitter.close(); Thread.sleep(5000); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java index e3282d1c..8f4d891f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Snowplow.java @@ -102,11 +102,11 @@ public static Tracker createTracker(TrackerConfiguration trackerConfig, * Create a Snowplow tracker with default configuration by providing three parameters. * * @param namespace unique identifier for the Tracker instance - * @param collectorUrl collector endpoint * @param appId application ID + * @param collectorUrl collector endpoint * @return the created Tracker */ - public static Tracker createTracker(String namespace, String collectorUrl, String appId) { + public static Tracker createTracker(String namespace, String appId, String collectorUrl) { TrackerConfiguration trackerConfig = new TrackerConfiguration(namespace, appId); NetworkConfiguration networkConfig = new NetworkConfiguration(collectorUrl); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index 14a7b04a..64634c57 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -48,21 +48,35 @@ public Subject(SubjectConfiguration subjectConfig) { setDomainSessionId(subjectConfig.getDomainSessionId()); } + /** + * Creates a Subject instance with default configuration (only the timezone is set). + */ + public Subject() { + this(new SubjectConfiguration()); + } + /** * Creates a new {@link Subject} object based on the map of another {@link Subject} object. * @param subject The subject from which the map is copied. */ public Subject(Subject subject){ - this.standardPairs.putAll(subject.getSubject()); + standardPairs.putAll(subject.getSubject()); } + /** + * @deprecated Use SubjectConfiguration class instead + * @return a SubjectBuilder object + */ + @Deprecated public static SubjectBuilder builder() { return new SubjectBuilder(); } /** * Builder for the Subject + * @deprecated Use SubjectConfiguration class instead */ + @Deprecated public static class SubjectBuilder { private String userId; // Optional diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index 0aad41f3..f709f779 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -32,17 +32,6 @@ public class Tracker { private Subject subject; private final TrackerParameters parameters; - /** - * Creates a new Snowplow Tracker. - * - * @param trackerConfig a TrackerConfiguration object - * @param emitter an Emitter - * - */ - public Tracker(TrackerConfiguration trackerConfig, Emitter emitter) { - this(trackerConfig, emitter, null); - } - /** * Creates a new Snowplow Tracker. * @@ -70,9 +59,22 @@ public Tracker(TrackerConfiguration trackerConfig, Emitter emitter, Subject subj } + /** + * Creates a new Snowplow Tracker. + * + * @param trackerConfig a TrackerConfiguration object + * @param emitter an Emitter + * + */ + public Tracker(TrackerConfiguration trackerConfig, Emitter emitter) { + this(trackerConfig, emitter, null); + } + /** * Builder for the Tracker + * @deprecated Use TrackerConfiguration class instead */ + @Deprecated public static class TrackerBuilder { private final Emitter emitter; // Required @@ -137,6 +139,14 @@ public Tracker build() { } } + /** + * @deprecated Use TrackerConfiguration class instead + * @param emitter Emitter object + * @param namespace unique tracker namespace + * @param appId application ID + * @return TrackerBuilder object + */ + @Deprecated public static TrackerBuilder builder(Emitter emitter, String namespace, String appId) { return new TrackerBuilder(emitter, namespace, appId); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index 5aef6073..9bd57f1e 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -62,6 +62,11 @@ public class BatchEmitter implements Emitter, Closeable { private final Map customRetryForStatusCodes; private final EmitterCallback callback; + /** + * @deprecated Use NetworkConfiguration/EmitterConfiguration classes instead + * @param Builder + */ + @Deprecated public static abstract class Builder> { protected abstract T self(); @@ -219,6 +224,11 @@ protected Builder2 self() { } } + /** + * @deprecated Use NetworkConfiguration/EmitterConfiguration classes instead + * @return Builder object + */ + @Deprecated public static Builder builder() { return new Builder2(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index eca2fa83..f78a074f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -49,6 +49,7 @@ public interface EventStore { * * @param needRetry if another attempt should be made to send the events * @param batchId the ID of the batch of events + * @return list of TrackerPayloads */ List cleanupAfterSendingAttempt(boolean needRetry, long batchId); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index bfe43cda..1346a44d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -24,6 +24,15 @@ public abstract class AbstractHttpClientAdapter implements HttpClientAdapter { protected final String url; + public AbstractHttpClientAdapter(String url) { + this.url = url.replaceFirst("/*$", ""); + } + + /** + * @deprecated Create HttpClientAdapter directly instead + * @param Builder + */ + @Deprecated public static abstract class Builder> { private String url; // Required @@ -48,10 +57,20 @@ protected Builder2 self() { } } + /** + * @deprecated Create HttpClientAdapter directly instead + * @return Builder object + */ + @Deprecated public static Builder builder() { return new Builder2(); } + /** + * @deprecated Create HttpClientAdapter directly instead + * @param builder Builder object + */ + @Deprecated protected AbstractHttpClientAdapter(Builder builder) { // Precondition checks if (!Utils.isValidUrl(builder.url)) { diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 417918a1..f2089cb0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -35,6 +35,20 @@ public class ApacheHttpClientAdapter extends AbstractHttpClientAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(ApacheHttpClientAdapter.class); private CloseableHttpClient httpClient; + public ApacheHttpClientAdapter(String url, CloseableHttpClient httpClient) { + super(url); + + // Precondition checks + Objects.requireNonNull(httpClient); + + this.httpClient = httpClient; + } + + /** + * @deprecated Create HttpClientAdapter directly instead + * @param Builder + */ + @Deprecated public static abstract class Builder> extends AbstractHttpClientAdapter.Builder { private CloseableHttpClient httpClient; // Required @@ -60,10 +74,20 @@ protected Builder2 self() { } } + /** + * @deprecated Create HttpClientAdapter directly instead + * @return Builder object + */ + @Deprecated public static Builder builder() { return new Builder2(); } + /** + * @deprecated Create HttpClientAdapter directly instead + * @param builder Builder object + */ + @Deprecated protected ApacheHttpClientAdapter(Builder builder) { super(builder); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 4db80155..d3e8ecc9 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -36,6 +36,20 @@ public class OkHttpClientAdapter extends AbstractHttpClientAdapter { private final MediaType JSON = MediaType.get(Constants.POST_CONTENT_TYPE); private OkHttpClient httpClient; + public OkHttpClientAdapter(String url, OkHttpClient httpClient) { + super(url); + + // Precondition checks + Objects.requireNonNull(httpClient); + + this.httpClient = httpClient; + } + + /** + * @deprecated Create HttpClientAdapter directly instead + * @param Builder + */ + @Deprecated public static abstract class Builder> extends AbstractHttpClientAdapter.Builder { private OkHttpClient httpClient; // Required @@ -61,10 +75,20 @@ protected Builder2 self() { } } + /** + * @deprecated Create HttpClientAdapter directly instead + * @return Builder object + */ + @Deprecated public static Builder builder() { return new Builder2(); } + /** + * @deprecated Create HttpClientAdapter directly instead + * @param builder Builder object + */ + @Deprecated protected OkHttpClientAdapter(Builder builder) { super(builder); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java index 5e6446dc..2734eb28 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java @@ -31,7 +31,7 @@ public void cleanUp(){ public void createsAndRetrievesATracker() { assertTrue(Snowplow.getInstancedTrackerNamespaces().isEmpty()); - Tracker tracker = Snowplow.createTracker("namespace", "http://endpoint", "appId"); + Tracker tracker = Snowplow.createTracker("namespace", "appId", "http://endpoint"); Tracker retrievedTracker = Snowplow.getTracker("namespace"); assertFalse(Snowplow.getInstancedTrackerNamespaces().isEmpty()); @@ -44,8 +44,8 @@ public void createsAndRetrievesATracker() { @Test public void preventsDuplicateNamespaces() { Exception exception = assertThrows(IllegalArgumentException.class, () -> { - Snowplow.createTracker("namespace", "http://endpoint", "appId"); - Snowplow.createTracker("namespace", "http://collector", "appId2"); + Snowplow.createTracker("namespace", "appId", "http://endpoint"); + Snowplow.createTracker("namespace", "appId2", "http://collector"); }); assertEquals("Tracker with this namespace already exists.", exception.getMessage()); @@ -53,19 +53,19 @@ public void preventsDuplicateNamespaces() { @Test public void deletesStoredTracker() { - Snowplow.createTracker("namespace", "http://endpoint", "appId"); + Snowplow.createTracker("namespace", "appId", "http://endpoint"); boolean result = Snowplow.removeTracker("namespace"); assertTrue(result); - Tracker tracker = Snowplow.createTracker("namespace2", "http://endpoint", "appId"); + Tracker tracker = Snowplow.createTracker("namespace2", "appId", "http://endpoint"); boolean result2 = Snowplow.removeTracker(tracker); assertTrue(result2); } @Test public void doesNotDeleteUnregisteredTracker() { - BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); - Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "appId").build(); + BatchEmitter emitter = new BatchEmitter(new NetworkConfiguration("http://collector")); + Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "appId"), emitter); boolean result = Snowplow.removeTracker(tracker); assertFalse(result); @@ -78,10 +78,10 @@ public void doesNotDeleteUnregisteredTracker() { public void setsDefaultTrackerFromObject() { assertNull(Snowplow.getDefaultTracker()); - Tracker tracker = Snowplow.createTracker("namespace", "http://endpoint", "appId"); + Tracker tracker = Snowplow.createTracker("namespace", "appId", "http://endpoint"); assertEquals(tracker, Snowplow.getDefaultTracker()); - Tracker tracker2 = Snowplow.createTracker("namespace2", "http://endpoint", "appId"); + Tracker tracker2 = Snowplow.createTracker("namespace2", "appId", "http://endpoint"); // The first tracker is still the default assertEquals(tracker, Snowplow.getDefaultTracker()); @@ -94,8 +94,8 @@ public void setsDefaultTrackerFromObject() { public void setsDefaultTrackerFromNamespace() { assertNull(Snowplow.getDefaultTracker()); - Snowplow.createTracker("namespace", "http://endpoint", "appId"); - Tracker tracker2 = Snowplow.createTracker("namespace2", "http://endpoint", "appId"); + Snowplow.createTracker("namespace", "appId", "http://endpoint"); + Tracker tracker2 = Snowplow.createTracker("namespace2", "appId", "http://endpoint"); boolean result = Snowplow.setDefaultTracker("namespace2"); assertTrue(result); @@ -104,8 +104,8 @@ public void setsDefaultTrackerFromNamespace() { @Test public void registersATrackerMadeWithoutSnowplowClass() { - BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); - Tracker tracker = new Tracker.TrackerBuilder(emitter, "namespace", "appId").build(); + BatchEmitter emitter = new BatchEmitter(new NetworkConfiguration("http://collector")); + Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "appId"), emitter); Snowplow.registerTracker(tracker); @@ -115,8 +115,8 @@ public void registersATrackerMadeWithoutSnowplowClass() { @Test public void settingNewDefaultTrackerRegistersIt() { - BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); - Tracker tracker = new Tracker.TrackerBuilder(emitter, "new_tracker", "appId").build(); + BatchEmitter emitter = new BatchEmitter(new NetworkConfiguration("http://collector")); + Tracker tracker = new Tracker(new TrackerConfiguration("new_tracker", "appId"), emitter); Snowplow.setDefaultTracker(tracker); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 8cbff08c..38bad0fe 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -25,84 +25,84 @@ public class SubjectTest { @Test public void testSetUserId() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setUserId("user1"); assertEquals("user1", subject.getSubject().get("uid")); } @Test public void testSetScreenResolution() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setScreenResolution(100, 150); assertEquals("100x150", subject.getSubject().get("res")); } @Test public void testSetViewPort() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setViewPort(150, 100); assertEquals("150x100", subject.getSubject().get("vp")); } @Test public void testSetColorDepth() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setColorDepth(10); assertEquals("10", subject.getSubject().get("cd")); } @Test public void testSetTimezone2() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setTimezone("America/Toronto"); assertEquals("America/Toronto", subject.getSubject().get("tz")); } @Test public void testSetLanguage() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setLanguage("EN"); assertEquals("EN", subject.getSubject().get("lang")); } @Test public void testSetIpAddress() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setIpAddress("127.0.0.1"); assertEquals("127.0.0.1", subject.getSubject().get("ip")); } @Test public void testSetUseragent() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setUseragent("useragent"); assertEquals("useragent", subject.getSubject().get("ua")); } @Test public void testSetDomainUserId() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setDomainUserId("duid"); assertEquals("duid", subject.getSubject().get("duid")); } @Test public void testSetNetworkUserId() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setNetworkUserId("nuid"); assertEquals("nuid", subject.getSubject().get("tnuid")); } @Test public void testSetDomainSessionId() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); subject.setDomainSessionId("sessionid"); assertEquals("sessionid", subject.getSubject().get("sid")); } @Test public void testGetSubject() { - Subject subject = Subject.builder().build(); + Subject subject = new Subject(); Map expected = new HashMap<>(); subject.setTimezone("America/Toronto"); subject.setUserId("user1"); @@ -113,27 +113,16 @@ public void testGetSubject() { assertEquals(expected, subject.getSubject()); } - @Test - public void testCreateWithBuilder() { - Subject subject = Subject.builder() - .domainSessionId("domain session ID") - .viewPort(123, 456) - .language("en") - .build(); - - assertEquals("domain session ID", subject.getSubject().get("sid")); - assertEquals("123x456", subject.getSubject().get("vp")); - assertEquals("en", subject.getSubject().get("lang")); - } - @Test public void testCreateFromConfig() { SubjectConfiguration subjectConfig = new SubjectConfiguration() .ipAddress("xxx.000.xxx.111") + .viewPort(123, 456) .useragent("Mac OS"); Subject subject = new Subject(subjectConfig); assertEquals("xxx.000.xxx.111", subject.getSubject().get("ip")); + assertEquals("123x456", subject.getSubject().get("vp")); assertEquals("Mac OS", subject.getSubject().get("ua")); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 320d6ece..68e732a0 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -56,10 +56,8 @@ public void flushBuffer() {} @Before public void setUp() { mockEmitter = new MockEmitter(); - tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") - .subject(Subject.builder().build()) - .base64(false) - .build(); + TrackerConfiguration trackerConfig = new TrackerConfiguration("AF003", "cloudfront").base64Encoded(false); + tracker = new Tracker(trackerConfig, mockEmitter, new Subject()); tracker.getSubject().setTimezone("Etc/UTC"); contexts = singletonList(new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar"))); } @@ -103,7 +101,7 @@ public void flushBuffer() {} public List getBuffer() { return null; } } FailingMockEmitter failingMockEmitter = new FailingMockEmitter(); - tracker = Tracker.builder(failingMockEmitter, "AF003", "cloudfront").build(); + tracker = new Tracker(new TrackerConfiguration("AF003", "cloudfront"), failingMockEmitter); List result = tracker.track(SelfDescribing.builder() .eventData(new SelfDescribingJson( @@ -324,12 +322,6 @@ public void testSelfDescribingEventWithoutTrueTimestamp() throws InterruptedExce @Test public void testTrackPageView() throws InterruptedException { - tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") - .subject(Subject.builder().build()) - .base64(false) - .build(); - tracker.getSubject().setTimezone("Etc/UTC"); - // When tracker.track(PageView.builder() .pageUrl("url") @@ -527,7 +519,7 @@ public void testTrackTiming() throws InterruptedException { @Test public void testTrackTimingWithSubject() throws InterruptedException { // Make Subject - Subject s1 = Subject.builder().build(); + Subject s1 = new Subject(); s1.setIpAddress("127.0.0.1"); s1.setTimezone("Etc/UTC"); @@ -569,7 +561,8 @@ public void testCreateWithConfiguration() { TrackerConfiguration trackerConfig = new TrackerConfiguration("namespace", "appId"); trackerConfig.base64Encoded(false); trackerConfig.platform(DevicePlatform.General); - BatchEmitter emitter = BatchEmitter.builder().url("http://collector").build(); + + BatchEmitter emitter = new BatchEmitter(new NetworkConfiguration("http://collector")); Tracker tracker = new Tracker(trackerConfig, emitter); assertEquals("namespace", tracker.getNamespace()); @@ -578,15 +571,16 @@ public void testCreateWithConfiguration() { @Test public void testGetTrackerVersion() { - Tracker tracker = Tracker.builder(mockEmitter, "namespace", "an-app-id").build(); + Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "an-app-id"), mockEmitter); assertEquals("java-0.12.2", tracker.getTrackerVersion()); } @Test public void testSetDefaultPlatform() { - Tracker tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") - .platform(DevicePlatform.Desktop) - .build(); + TrackerConfiguration trackerConfig = new TrackerConfiguration("AF003", "cloudfront") + .platform(DevicePlatform.Desktop); + + Tracker tracker = new Tracker(trackerConfig, mockEmitter); assertEquals(DevicePlatform.Desktop, tracker.getPlatform()); } @@ -595,13 +589,11 @@ public void testSetSubject() { // Subject objects always have timezone set TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); - Subject s1 = Subject.builder().build(); + Subject s1 = new Subject(); s1.setLanguage("EN"); - Tracker tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") - .subject(s1) - .build(); + Tracker tracker = new Tracker(new TrackerConfiguration("AF003", "cloudfront"), mockEmitter, s1); - Subject s2 = Subject.builder().build(); + Subject s2 = new Subject(); s2.setColorDepth(24); tracker.setSubject(s2); @@ -614,21 +606,22 @@ public void testSetSubject() { @Test public void testSetBase64Encoded() { - Tracker tracker = Tracker.builder(mockEmitter, "AF003", "cloudfront") - .base64(false) - .build(); + TrackerConfiguration trackerConfig = new TrackerConfiguration("AF003", "cloudfront").base64Encoded(false); + tracker = new Tracker(trackerConfig, mockEmitter); + assertFalse(tracker.getBase64Encoded()); } @Test public void testSetAppId() { - Tracker tracker = Tracker.builder(mockEmitter, "AF003", "an-app-id").build(); + Tracker tracker = new Tracker(new TrackerConfiguration("AF003", "an-app-id"), mockEmitter); assertEquals("an-app-id", tracker.getAppId()); } @Test public void testSetNamespace() { - Tracker tracker = Tracker.builder(mockEmitter, "namespace", "an-app-id").build(); + Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "an-app-id"), mockEmitter); + assertEquals("namespace", tracker.getNamespace()); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index 0aeb57f3..e2d35971 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -93,10 +93,8 @@ public int post(SelfDescribingJson payload) { @Before public void setUp() { mockHttpClientAdapter = new MockHttpClientAdapter(200); - emitter = BatchEmitter.builder() - .httpClientAdapter(mockHttpClientAdapter) - .batchSize(10) - .build(); + EmitterConfiguration emitterConfig = new EmitterConfiguration().batchSize(10); + emitter = new BatchEmitter(new NetworkConfiguration(mockHttpClientAdapter), emitterConfig); } @Test @@ -129,11 +127,9 @@ public void addToBuffer_withMore10Payloads_shouldEmptyBuffer() throws Interrupte @Test public void addToBuffer_doesNotAddEventIfBufferFull() { - emitter = BatchEmitter.builder() - .httpClientAdapter(mockHttpClientAdapter) - .bufferCapacity(1) - .build(); - + emitter = new BatchEmitter( + new NetworkConfiguration(mockHttpClientAdapter), + new EmitterConfiguration().bufferCapacity(1)); emitter.add(createPayload()); TrackerPayload differentPayload = createPayload(); @@ -213,7 +209,7 @@ public void threadsHaveExpectedNames() { threadNames.add(thread.getName()); } - // Because the threadpools are named by a static ThreadFactory, + // Because the thread pools are named by a static ThreadFactory, // the pool number varies if this test is run in isolation or not boolean matchResult = false; for (String name : threadNames) { @@ -260,10 +256,9 @@ public void createEmitterWithConfiguration() { @Test public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedException { - emitter = BatchEmitter.builder() - .httpClientAdapter(new FlakyHttpClientAdapter()) - .batchSize(10) - .build(); + emitter = new BatchEmitter( + new NetworkConfiguration(new FlakyHttpClientAdapter()), + new EmitterConfiguration().batchSize(10)); List payloads = createPayloads(2); for (TrackerPayload payload : payloads) { @@ -281,10 +276,9 @@ public void eventsThatFailToSendAreReturnedToEventBuffer() throws InterruptedExc @Test public void eventSendingFailureIncreasesBackoffTime() throws InterruptedException { - emitter = BatchEmitter.builder() - .httpClientAdapter(new MockHttpClientAdapter(500)) - .batchSize(1) - .build(); + emitter = new BatchEmitter( + new NetworkConfiguration(new MockHttpClientAdapter(500)), + new EmitterConfiguration().batchSize(1)); emitter.add(createPayload()); Thread.sleep(500); @@ -304,11 +298,9 @@ public void successfulSendAfterFailureResetsBackoffTime() throws InterruptedExce // the FlakyHttpClientAdapter returns 500 for the first 4 requests // then subsequently returns 200 FlakyHttpClientAdapter flakyHttpClientAdapter = new FlakyHttpClientAdapter(); - emitter = BatchEmitter.builder() - .httpClientAdapter(flakyHttpClientAdapter) - .batchSize(1) - .threadCount(1) - .build(); + emitter = new BatchEmitter( + new NetworkConfiguration(flakyHttpClientAdapter), + new EmitterConfiguration().batchSize(1).threadCount(1)); List payloads = createPayloads(6); for (TrackerPayload payload : payloads) { @@ -327,11 +319,9 @@ public void retryWithCustomRulesOverridingDefault() throws InterruptedException customRetry.put(403, true); // by default 403 isn't retried - BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(new MockHttpClientAdapter(403)) - .customRetryForStatusCodes(customRetry) - .batchSize(2) - .build(); + BatchEmitter emitter = new BatchEmitter( + new NetworkConfiguration(new MockHttpClientAdapter(403)), + new EmitterConfiguration().batchSize(2).customRetryForStatusCodes(customRetry)); List payloads = createPayloads(4); for (TrackerPayload payload : payloads) { @@ -349,11 +339,9 @@ public void noRetryWithCustomRulesOverridingDefault() throws InterruptedExceptio customRetry.put(500, false); // by default, requests with code 500 are retried - BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(new MockHttpClientAdapter(500)) - .customRetryForStatusCodes(customRetry) - .batchSize(2) - .build(); + BatchEmitter emitter = new BatchEmitter( + new NetworkConfiguration(new MockHttpClientAdapter(500)), + new EmitterConfiguration().batchSize(2).customRetryForStatusCodes(customRetry)); List payloads = createPayloads(4); for (TrackerPayload payload : payloads) { @@ -381,14 +369,12 @@ public void onSuccess(List payloads) { public void onFailure(FailureType failureType, boolean willRetry, List payloads) { failureTypes.add(failureType); } - }; + } TestCallback callback = new TestCallback(); - BatchEmitter emitter = BatchEmitter.builder() - .httpClientAdapter(new MockHttpClientAdapter(200)) - .batchSize(1) - .callback(callback) - .build(); + BatchEmitter emitter = new BatchEmitter( + new NetworkConfiguration(new MockHttpClientAdapter(200)), + new EmitterConfiguration().batchSize(1).callback(callback)); TrackerPayload payload = createPayload(); emitter.add(payload); @@ -415,14 +401,12 @@ public void onFailure(FailureType failureType, boolean willRetry, List data() { {new HttpClientAdapterProvider() { @Override public HttpClientAdapter provide(String url) { - return ApacheHttpClientAdapter.builder() - .url(url) - .httpClient(HttpClients.createDefault()) - .build(); + return new ApacheHttpClientAdapter(url, HttpClients.createDefault()); } }}, {new HttpClientAdapterProvider() { @@ -64,10 +61,7 @@ public HttpClientAdapter provide(String url) { .readTimeout(1, TimeUnit.SECONDS) .writeTimeout(1, TimeUnit.SECONDS) .build(); - return OkHttpClientAdapter.builder() - .url(url) - .httpClient(httpClient) - .build(); + return new OkHttpClientAdapter(url, httpClient); } } } @@ -138,10 +132,7 @@ public void testRequestWithCookies() throws IOException, InterruptedException { .writeTimeout(1, TimeUnit.SECONDS) .cookieJar(new CollectorCookieJar()) .build(); - adapter = OkHttpClientAdapter.builder() - .url(mockWebServer.url("/").toString()) - .httpClient(httpClient) - .build(); + adapter = new OkHttpClientAdapter(mockWebServer.url("/").toString(), httpClient); mockWebServer.enqueue(new MockResponse().addHeader("Set-Cookie", "sp=test")); From 8550a50bd29fd8097646345e8c0dbaba364340e0 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 25 Aug 2022 17:32:01 +0100 Subject: [PATCH 109/128] Add close() to Emitter interface and Tracker (close #357) PR #358 * Add close() to Emitter interface and Tracker * Correct docs for FailureType --- .../src/main/java/com/snowplowanalytics/Main.java | 5 ++--- .../snowplowanalytics/snowplow/tracker/Tracker.java | 10 ++++++++++ .../snowplow/tracker/emitter/Emitter.java | 5 +++++ .../snowplow/tracker/emitter/FailureType.java | 2 +- .../snowplow/tracker/TrackerTest.java | 5 +++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index 9815d240..d901f34f 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -19,7 +19,6 @@ import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; -import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; import com.snowplowanalytics.snowplow.tracker.events.*; import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; @@ -77,6 +76,7 @@ public static void main(String[] args) throws InterruptedException { .subject(eventSubject) .build(); + // EcommerceTransactions will be deprecated soon: we advise using SelfDescribing events instead // EcommerceTransactionItems are tracked as part of an EcommerceTransaction event // They are processed into separate events during the `track()` call EcommerceTransactionItem item = EcommerceTransactionItem.builder() @@ -151,8 +151,7 @@ public static void main(String[] args) throws InterruptedException { tracker.track(structured); // Will close all threads and force send remaining events - BatchEmitter emitter = (BatchEmitter) tracker.getEmitter(); - emitter.close(); + tracker.close(); Thread.sleep(5000); System.out.println("Tracked 7 events"); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index f709f779..0ff934d2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -356,4 +356,14 @@ private void addSubject(Event event, TrackerPayload payload) { } } + /** + * Attempts to send all remaining events, then shuts down the Emitter so that no more events can be sent. + * This method calls the Emitter.close() method. For the default BatchEmitter, + * this stops the ScheduledExecutorService and the thread pool (non-daemon threads). + * There is no way to restart the Emitter after calling close(). + */ + public void close() { + emitter.close(); + } + } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index 14e80ff2..58a74240 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -57,4 +57,9 @@ public interface Emitter { * @return the buffer events */ List getBuffer(); + + /** + * Safely shuts down the Emitter. + */ + void close(); } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java index 487c25f5..b3c2cd95 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java @@ -33,7 +33,7 @@ public enum FailureType { TRACKER_STORAGE_FULL, /** - * An exception or unsuccessful POST request in OkHttpClientAdapter. + * An exception or unsuccessful POST request in the HttpClientAdapter. */ HTTP_CONNECTION_FAILURE, diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 68e732a0..b2d79ded 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -15,6 +15,7 @@ import java.util.*; import static java.util.Collections.singletonList; +import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; @@ -47,6 +48,8 @@ public void flushBuffer() {} public int getBatchSize() { return 0; } @Override public List getBuffer() { return null; } + @Override + public void close() {} } MockEmitter mockEmitter; @@ -99,6 +102,8 @@ public void flushBuffer() {} public int getBatchSize() { return 0; } @Override public List getBuffer() { return null; } + @Override + public void close() {} } FailingMockEmitter failingMockEmitter = new FailingMockEmitter(); tracker = new Tracker(new TrackerConfiguration("AF003", "cloudfront"), failingMockEmitter); From 4cbf9e88c2fe1ed7cddd965febd2e2a1048d5f05 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 25 Aug 2022 18:57:29 +0100 Subject: [PATCH 110/128] Prepare for 1.0.0 release Update classification badge Update dependency for simple-console Fix Maintained badge in README --- CHANGELOG | 16 ++++++++++++++++ README.md | 4 ++-- build.gradle | 2 +- examples/simple-console/build.gradle | 4 ++-- .../snowplow/tracker/TrackerTest.java | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 98b76064..ca3cdbdc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,19 @@ +Java 1.0.0 (2022-09-06) +----------------------- +Add close() to Emitter interface and Tracker (#357) +Deprecate Builder classes (#355) +Reduce the default maximum event buffer capacity (#352) +Add admin workflow for automatic issue labelling (#346) +Remove SimpleEmitter (#341) +Add a Snowplow interface with the ability to initialize and manage multiple trackers (#340) +Restore Emitter callbacks for success and failure (#339) +Add a maximum wait time and jitter to event sending retry (#338) +Set default HTTP status codes not to retry on (#337) +Add support for storing cookies in OkHttpClientAdapter (#336) +Remove Guava dependency (#320) +Standardise API for Tracker and Subject Builders (#302) +Rename Unstructured events to SelfDescribing (#296) + Java 0.12.2 (2022-06-17) ----------------------- Bump jackson-databind to 2.13.3 (#333) diff --git a/README.md b/README.md index 4a9191fc..d08123eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Java Analytics for Snowplow -[![early-release]][tracker-classification] [![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] +[![maintained]][tracker-classification] [![Build][github-image]][github] [![Release][release-image]][releases] [![License][license-image]][license] ## Overview @@ -88,4 +88,4 @@ limitations under the License. [apidocs]: https://snowplow.github.io/snowplow-java-tracker/index.html?overview-summary.html [tracker-classification]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/tracker-maintenance-classification/ -[early-release]: https://img.shields.io/static/v1?style=flat&label=Snowplow&message=Early%20Release&color=014477&labelColor=9ba0aa&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEVMaXGXANeYANeXANZbAJmXANeUANSQAM+XANeMAMpaAJhZAJeZANiXANaXANaOAM2WANVnAKWXANZ9ALtmAKVaAJmXANZaAJlXAJZdAJxaAJlZAJdbAJlbAJmQAM+UANKZANhhAJ+EAL+BAL9oAKZnAKVjAKF1ALNBd8J1AAAAKHRSTlMAa1hWXyteBTQJIEwRgUh2JjJon21wcBgNfmc+JlOBQjwezWF2l5dXzkW3/wAAAHpJREFUeNokhQOCA1EAxTL85hi7dXv/E5YPCYBq5DeN4pcqV1XbtW/xTVMIMAZE0cBHEaZhBmIQwCFofeprPUHqjmD/+7peztd62dWQRkvrQayXkn01f/gWp2CrxfjY7rcZ5V7DEMDQgmEozFpZqLUYDsNwOqbnMLwPAJEwCopZxKttAAAAAElFTkSuQmCC +[maintained]: https://img.shields.io/static/v1?style=flat&label=Snowplow&message=Maintained&color=9e62dd&labelColor=9ba0aa&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEVMaXGXANeYANeXANZbAJmXANeUANSQAM+XANeMAMpaAJhZAJeZANiXANaXANaOAM2WANVnAKWXANZ9ALtmAKVaAJmXANZaAJlXAJZdAJxaAJlZAJdbAJlbAJmQAM+UANKZANhhAJ+EAL+BAL9oAKZnAKVjAKF1ALNBd8J1AAAAKHRSTlMAa1hWXyteBTQJIEwRgUh2JjJon21wcBgNfmc+JlOBQjwezWF2l5dXzkW3/wAAAHpJREFUeNokhQOCA1EAxTL85hi7dXv/E5YPCYBq5DeN4pcqV1XbtW/xTVMIMAZE0cBHEaZhBmIQwCFofeprPUHqjmD/+7peztd62dWQRkvrQayXkn01f/gWp2CrxfjY7rcZ5V7DEMDQgmEozFpZqLUYDsNwOqbnMLwPAJEwCopZxKttAAAAAElFTkSuQmCC diff --git a/build.gradle b/build.gradle index 6e99d26f..5d991b54 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '0.12.2' +version = '1.0.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index b021e09b..78d31acc 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -16,9 +16,9 @@ test { } dependencies { - implementation 'com.snowplowanalytics:snowplow-java-tracker:0.+' + implementation 'com.snowplowanalytics:snowplow-java-tracker:1.+' - implementation ('com.snowplowanalytics:snowplow-java-tracker:0.+') { + implementation ('com.snowplowanalytics:snowplow-java-tracker:1.+') { capabilities { requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support' } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index b2d79ded..208a5afc 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -577,7 +577,7 @@ public void testCreateWithConfiguration() { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "an-app-id"), mockEmitter); - assertEquals("java-0.12.2", tracker.getTrackerVersion()); + assertEquals("java-1.0.0", tracker.getTrackerVersion()); } @Test From 8b629a48ec1d6d49413e943c3441b43a141599b7 Mon Sep 17 00:00:00 2001 From: eusorov Date: Thu, 26 Oct 2023 12:42:20 +0200 Subject: [PATCH 111/128] Fix Issue with OkHttpClientAdapter (closes #366) --- .../tracker/http/OkHttpClientAdapter.java | 6 ++---- .../tracker/http/HttpClientAdapterTest.java | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index d3e8ecc9..32eeaecc 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -123,9 +123,8 @@ public int doGet(String url) { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { LOGGER.error("OkHttpClient GET Request failed: {}", response); - } else { - returnValue = response.code(); } + returnValue = response.code(); } catch (IOException e) { LOGGER.error("OkHttpClient GET Request failed: {}", e.getMessage()); } @@ -154,9 +153,8 @@ public int doPost(String url, String payload) { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { LOGGER.error("OkHttpClient POST Request failed: {}", response); - } else { - returnValue = response.code(); } + returnValue = response.code(); } catch (IOException e) { LOGGER.error("OkHttpClient POST Request failed: {}", e.getMessage()); } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 710aa6c9..a6d42dd5 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -103,9 +103,28 @@ public void post_withSuccessfulStatusCode_isOk() throws InterruptedException { mockWebServer.enqueue(new MockResponse().setResponseCode(200)); // When - adapter.post(new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar"))); + int responseCode = adapter.post(new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar"))); // Then + assertEquals(200, responseCode); + assertEquals(1, mockWebServer.getRequestCount()); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertEquals("/com.snowplowanalytics.snowplow/tp2", recordedRequest.getPath()); + assertEquals("{\"schema\":\"schema\",\"data\":{\"foo\":\"bar\"}}", recordedRequest.getBody().readUtf8()); + assertEquals("POST", recordedRequest.getMethod()); + assertEquals("application/json; charset=utf-8", recordedRequest.getHeader("Content-Type")); + } + + @Test + public void post_withUnsuccessfulStatusCode_isOk() throws InterruptedException { + // Given + mockWebServer.enqueue(new MockResponse().setResponseCode(404)); + + // When + int responseCode = adapter.post(new SelfDescribingJson("schema", Collections.singletonMap("foo", "bar"))); + + // Then + assertEquals(404, responseCode); assertEquals(1, mockWebServer.getRequestCount()); RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertEquals("/com.snowplowanalytics.snowplow/tp2", recordedRequest.getPath()); From f0708e6adad250283403ffa7279ac4c7ec6f58b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Fri, 3 Nov 2023 16:00:09 +0100 Subject: [PATCH 112/128] Prepare for 1.0.1 release --- CHANGELOG | 4 ++++ build.gradle | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ca3cdbdc..b62404a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +Java 1.0.1 (2023-11-06) +----------------------- +Fix Issue with OkHttpClientAdapter (#366) (thanks to @eusorov for the contribution!) + Java 1.0.0 (2022-09-06) ----------------------- Add close() to Emitter interface and Tracker (#357) diff --git a/build.gradle b/build.gradle index 5d991b54..ff359110 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '1.0.0' +version = '1.0.1' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 208a5afc..80d3b27b 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -577,7 +577,7 @@ public void testCreateWithConfiguration() { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "an-app-id"), mockEmitter); - assertEquals("java-1.0.0", tracker.getTrackerVersion()); + assertEquals("java-1.0.1", tracker.getTrackerVersion()); } @Test From 94e9c52e8628b621124ee025486a449b47dbd342 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 11 Jan 2024 08:04:49 +0100 Subject: [PATCH 113/128] Add builder methods Subject to allow method chaining (close #303) PR #369 --- .../snowplow/tracker/Subject.java | 138 +++++++++++++++++- .../snowplow/tracker/SubjectTest.java | 28 ++++ 2 files changed, 158 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index 64634c57..35bc7299 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -232,6 +232,17 @@ public void setUserId(String userId) { } } + /** + * Sets the User ID and returns itself + * + * @param userId a user id string + * @return itself + */ + public Subject userId(String userId) { + this.setUserId(userId); + return this; + } + /** * Sets the screen res parameter * @@ -245,6 +256,18 @@ public void setScreenResolution(int width, int height) { } } + /** + * Sets the screen res parameter and returns itself + * + * @param width a width integer + * @param height a height integer + * @return itself + */ + public Subject screenResolution(int width, int height) { + this.setScreenResolution(width, height); + return this; + } + /** * Sets the view port parameter * @@ -258,6 +281,18 @@ public void setViewPort(int width, int height) { } } + /** + * Sets the view port parameter and returns itself + * + * @param width a width integer + * @param height a height integer + * @return itself + */ + public Subject viewPort(int width, int height) { + this.setViewPort(width, height); + return this; + } + /** * Sets the color depth parameter * @@ -269,6 +304,17 @@ public void setColorDepth(int depth) { } } + /** + * Sets the color depth parameter and returns itself + * + * @param depth a color depth integer + * @return itself + */ + public Subject colorDepth(int depth) { + this.setColorDepth(depth); + return this; + } + /** * Sets the timezone parameter. Note that timezone is set by default to the server's timezone * (`TimeZone tz = Calendar.getInstance().getTimeZone().getID()`); @@ -281,6 +327,19 @@ public void setTimezone(String timezone) { } } + /** + * Sets the timezone parameter and returns itself. + * Note that timezone is set by default to the server's timezone + * (`TimeZone tz = Calendar.getInstance().getTimeZone().getID()`) + * + * @param timezone a timezone string + * @return itself + */ + public Subject timezone(String timezone) { + this.setTimezone(timezone); + return this; + } + /** * Sets the language parameter * @@ -293,8 +352,18 @@ public void setLanguage(String language) { } /** - * User inputted ip address for the - * subject. + * Sets the language parameter and returns itself + * + * @param language a language string + * @return itself + */ + public Subject language(String language) { + this.setLanguage(language); + return this; + } + + /** + * User inputted ip address for the subject. * * @param ipAddress an ip address */ @@ -305,8 +374,18 @@ public void setIpAddress(String ipAddress) { } /** - * User inputted useragent for the - * subject. + * Sets the user inputted ip address for the subject and returns itself + * + * @param ipAddress a ipAddress string + * @return itself + */ + public Subject ipAddress(String ipAddress) { + this.setIpAddress(ipAddress); + return this; + } + + /** + * User inputted useragent for the subject. * * @param useragent a useragent */ @@ -317,8 +396,18 @@ public void setUseragent(String useragent) { } /** - * User inputted Domain User Id for the - * subject. + * Sets the user inputted useragent for the subject and returns itself + * + * @param useragent a useragent string + * @return itself + */ + public Subject useragent(String useragent) { + this.setUseragent(useragent); + return this; + } + + /** + * User inputted Domain User Id for the subject. * * @param domainUserId a domain user id */ @@ -329,8 +418,18 @@ public void setDomainUserId(String domainUserId) { } /** - * User inputted Domain Session ID for the - * subject. + * Sets the user inputted Domain User Id for the subject and returns itself + * + * @param domainUserId a domainUserId string + * @return itself + */ + public Subject domainUserId(String domainUserId) { + this.setDomainUserId(domainUserId); + return this; + } + + /** + * User inputted Domain Session ID for the subject. * * @param domainSessionId a domain session id */ @@ -340,6 +439,17 @@ public void setDomainSessionId(String domainSessionId) { } } + /** + * Sets the user inputted Domain Session ID for the subject and returns itself + * + * @param domainSessionId a domainSessionId string + * @return itself + */ + public Subject domainSessionId(String domainSessionId) { + this.setDomainSessionId(domainSessionId); + return this; + } + /** * User inputted Network User ID for the subject. * This overrides the network user ID set by the Collector in response Cookies. @@ -352,6 +462,18 @@ public void setNetworkUserId(String networkUserId) { } } + /** + * Sets the user inputted Network User ID for the subject and returns itself. + * This overrides the network user ID set by the Collector in response Cookies. + * + * @param networkUserId a networkUserId string + * @return itself + */ + public Subject networkUserId(String networkUserId) { + this.setNetworkUserId(networkUserId); + return this; + } + /** * Gets the Subject pairs. * diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 38bad0fe..954636c0 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -113,6 +113,34 @@ public void testGetSubject() { assertEquals(expected, subject.getSubject()); } + @Test + public void testBuilderMethods() { + Subject subject = new Subject(); + subject + .userId("user1") + .screenResolution(100, 150) + .viewPort(150, 100) + .colorDepth(10) + .timezone("America/Toronto") + .language("EN") + .ipAddress("127.0.0.1") + .useragent("useragent") + .domainUserId("duid") + .domainSessionId("sessionid") + .networkUserId("nuid"); + assertEquals("user1", subject.getSubject().get("uid")); + assertEquals("100x150", subject.getSubject().get("res")); + assertEquals("150x100", subject.getSubject().get("vp")); + assertEquals("10", subject.getSubject().get("cd")); + assertEquals("America/Toronto", subject.getSubject().get("tz")); + assertEquals("EN", subject.getSubject().get("lang")); + assertEquals("127.0.0.1", subject.getSubject().get("ip")); + assertEquals("useragent", subject.getSubject().get("ua")); + assertEquals("duid", subject.getSubject().get("duid")); + assertEquals("sessionid", subject.getSubject().get("sid")); + assertEquals("nuid", subject.getSubject().get("tnuid")); + } + @Test public void testCreateFromConfig() { SubjectConfiguration subjectConfig = new SubjectConfiguration() From 8a03602ec712ab44b82346e565d4fedbb7b16b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 10 Jan 2024 14:50:35 +0100 Subject: [PATCH 114/128] Bump commons-codec to 1.16 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ff359110..7b703a51 100644 --- a/build.gradle +++ b/build.gradle @@ -58,8 +58,8 @@ test { dependencies { // Apache Commons - api 'commons-codec:commons-codec:1.15' api 'commons-net:commons-net:3.6' + api 'commons-codec:commons-codec:1.16.0' // Apache HTTP apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.13' From 53749e6da4d14434c3b4cbaa5929392db0bfa854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 10 Jan 2024 14:51:02 +0100 Subject: [PATCH 115/128] Bump commons-net to 3.10 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7b703a51..2de001a0 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ test { dependencies { // Apache Commons - api 'commons-net:commons-net:3.6' + api 'commons-net:commons-net:3.10.0' api 'commons-codec:commons-codec:1.16.0' // Apache HTTP From 9345e2b665c4ad0634c56d50c1f42f27aea6d5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 10 Jan 2024 14:52:05 +0100 Subject: [PATCH 116/128] Bump jackson-databind to 2.16.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2de001a0..0bd181e8 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ dependencies { testImplementation 'org.slf4j:slf4j-simple:1.7.36' // Jackson JSON processor - api 'com.fasterxml.jackson.core:jackson-databind:2.13.3' + api 'com.fasterxml.jackson.core:jackson-databind:2.16.1' // Testing libraries testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' From 7497813689e708f4fb7bc9744fe87d26766489f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 10 Jan 2024 14:53:00 +0100 Subject: [PATCH 117/128] Bump junit-jupiter-api to 5.10.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0bd181e8..1f0e57b4 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ dependencies { api 'com.fasterxml.jackson.core:jackson-databind:2.16.1' // Testing libraries - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1' testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' From 4161f838b789b72ee7918f69424e0d4ec5fd545b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 10 Jan 2024 14:56:22 +0100 Subject: [PATCH 118/128] Bump slf4j-simple and slf4j-api to 2.0.11 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1f0e57b4..b01e82b0 100644 --- a/build.gradle +++ b/build.gradle @@ -69,8 +69,8 @@ dependencies { okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.9.3' // SLF4J logging API - api 'org.slf4j:slf4j-api:1.7.36' - testImplementation 'org.slf4j:slf4j-simple:1.7.36' + api 'org.slf4j:slf4j-api:2.0.11' + testImplementation 'org.slf4j:slf4j-simple:2.0.11' // Jackson JSON processor api 'com.fasterxml.jackson.core:jackson-databind:2.16.1' From 615ad6236b0f1d90c4103e5bca49602b125590c3 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 11 Jan 2024 08:05:38 +0100 Subject: [PATCH 119/128] Upgrade okhttp dependency to version 4.12 (close #365) PR #371 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b01e82b0..ce73664f 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.5' // Square OK HTTP - okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.9.3' + okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.12.0' // SLF4J logging API api 'org.slf4j:slf4j-api:2.0.11' @@ -80,7 +80,7 @@ dependencies { testCompileOnly 'junit:junit:4.13.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.3' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' } task sourceJar(type: Jar, dependsOn: 'generateSources') { From 8624fe920646bfb8b0965f821fc862b3d0c48e39 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 11 Jan 2024 08:06:15 +0100 Subject: [PATCH 120/128] Update to Apache Http Client to v5 (close #364) PR #370 --- build.gradle | 3 +-- .../tracker/http/ApacheHttpClientAdapter.java | 23 +++++++++---------- .../tracker/http/HttpClientAdapterTest.java | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index ce73664f..5d38bc7c 100644 --- a/build.gradle +++ b/build.gradle @@ -62,8 +62,7 @@ dependencies { api 'commons-codec:commons-codec:1.16.0' // Apache HTTP - apachehttpSupportApi 'org.apache.httpcomponents:httpclient:4.5.13' - apachehttpSupportApi 'org.apache.httpcomponents:httpasyncclient:4.1.5' + apachehttpSupportApi 'org.apache.httpcomponents.client5:httpclient5:5.3' // Square OK HTTP okhttpSupportApi 'com.squareup.okhttp3:okhttp:4.12.0' diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index f2089cb0..ad83f5f2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -12,12 +12,11 @@ */ package com.snowplowanalytics.snowplow.tracker.http; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.StringEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -117,9 +116,9 @@ public Object getHttpClient() { public int doGet(String url) { try { HttpGet httpGet = new HttpGet(url); - HttpResponse httpResponse = httpClient.execute(httpGet); - httpGet.releaseConnection(); - return httpResponse.getStatusLine().getStatusCode(); + return httpClient.execute(httpGet, response -> { + return response.getCode(); + }); } catch (Exception e) { LOGGER.error("ApacheHttpClient GET Request failed: {}", e.getMessage()); return -1; @@ -140,9 +139,9 @@ public int doPost(String url, String payload) { httpPost.addHeader("Content-Type", Constants.POST_CONTENT_TYPE); StringEntity params = new StringEntity(payload, ContentType.APPLICATION_JSON); httpPost.setEntity(params); - HttpResponse httpResponse = httpClient.execute(httpPost); - httpPost.releaseConnection(); - return httpResponse.getStatusLine().getStatusCode(); + return httpClient.execute(httpPost, response -> { + return response.getCode(); + }); } catch (Exception e) { LOGGER.error("ApacheHttpClient POST Request failed: {}", e.getMessage()); return -1; diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index a6d42dd5..7c9a8db7 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -23,7 +23,7 @@ import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.apache.http.impl.client.HttpClients; +import org.apache.hc.client5.http.impl.classic.HttpClients; import org.junit.Assert; import org.junit.Test; From 25a41bab2b9e7f5879bf48ddc2dfabf40ed1162c Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 11 Jan 2024 08:06:49 +0100 Subject: [PATCH 121/128] Update copyright headers in source files (#375) --- LICENSE | 2 +- README.md | 2 +- build.gradle | 4 ++-- examples/benchmarking/build.gradle | 2 +- .../src/jmh/java/com/snowplowanalytics/TrackerBenchmark.java | 2 +- .../src/main/java/com/snowplowanalytics/Main.java | 2 +- .../src/test/java/com/snowplowanalytics/MainTest.java | 2 +- .../snowplowanalytics/snowplow/tracker/DevicePlatform.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Snowplow.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Subject.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Tracker.java | 2 +- .../java/com/snowplowanalytics/snowplow/tracker/Utils.java | 2 +- .../snowplow/tracker/configuration/EmitterConfiguration.java | 2 +- .../snowplow/tracker/configuration/NetworkConfiguration.java | 2 +- .../snowplow/tracker/configuration/SubjectConfiguration.java | 2 +- .../snowplow/tracker/configuration/TrackerConfiguration.java | 2 +- .../snowplow/tracker/constants/Constants.java | 2 +- .../snowplow/tracker/constants/Parameter.java | 2 +- .../snowplow/tracker/emitter/BatchEmitter.java | 2 +- .../snowplow/tracker/emitter/BatchPayload.java | 2 +- .../snowplowanalytics/snowplow/tracker/emitter/Emitter.java | 2 +- .../snowplow/tracker/emitter/EmitterCallback.java | 2 +- .../snowplow/tracker/emitter/EventStore.java | 2 +- .../snowplow/tracker/emitter/FailureType.java | 2 +- .../snowplow/tracker/emitter/InMemoryEventStore.java | 2 +- .../snowplow/tracker/events/AbstractEvent.java | 2 +- .../snowplow/tracker/events/EcommerceTransaction.java | 2 +- .../snowplow/tracker/events/EcommerceTransactionItem.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Event.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/PageView.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/ScreenView.java | 2 +- .../snowplow/tracker/events/SelfDescribing.java | 2 +- .../snowplowanalytics/snowplow/tracker/events/Structured.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/events/Timing.java | 2 +- .../snowplow/tracker/http/AbstractHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/ApacheHttpClientAdapter.java | 2 +- .../snowplow/tracker/http/CollectorCookie.java | 2 +- .../snowplow/tracker/http/CollectorCookieJar.java | 2 +- .../snowplow/tracker/http/HttpClientAdapter.java | 2 +- .../snowplow/tracker/http/OkHttpClientAdapter.java | 2 +- .../snowplowanalytics/snowplow/tracker/payload/Payload.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJson.java | 2 +- .../snowplow/tracker/payload/TrackerParameters.java | 2 +- .../snowplow/tracker/payload/TrackerPayload.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/SnowplowTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/SubjectTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- .../com/snowplowanalytics/snowplow/tracker/UtilsTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterBuilderTest.java | 2 +- .../snowplow/tracker/emitter/BatchEmitterTest.java | 2 +- .../snowplow/tracker/emitter/CollectorCookieJarTest.java | 2 +- .../snowplow/tracker/emitter/InMemoryEventStoreTest.java | 2 +- .../snowplow/tracker/http/HttpClientAdapterTest.java | 2 +- .../snowplow/tracker/payload/SelfDescribingJsonTest.java | 2 +- .../snowplow/tracker/payload/TrackerPayloadTest.java | 2 +- 55 files changed, 56 insertions(+), 56 deletions(-) diff --git a/LICENSE b/LICENSE index e0977a71..0e1f4fe1 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 Snowplow Analytics Ltd. + Copyright 2014-present Snowplow Analytics Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index d08123eb..839e8fd3 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ $ java -jar ./build/libs/simple-console-all-0.0.1.jar "http:// Date: Thu, 11 Jan 2024 08:07:46 +0100 Subject: [PATCH 122/128] Add okhttp adapter with cookie jar and remove cookie jar from network configuration (close #361) PR #372 --- .../configuration/NetworkConfiguration.java | 22 ------------- .../tracker/emitter/BatchEmitter.java | 33 ++----------------- .../tracker/http/OkHttpClientAdapter.java | 4 +++ .../OkHttpClientWithCookieJarAdapter.java | 29 ++++++++++++++++ .../tracker/http/HttpClientAdapterTest.java | 8 +---- 5 files changed, 36 insertions(+), 60 deletions(-) create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientWithCookieJarAdapter.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java index 2f0915d9..7c33d101 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/configuration/NetworkConfiguration.java @@ -13,14 +13,12 @@ package com.snowplowanalytics.snowplow.tracker.configuration; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; -import okhttp3.CookieJar; public class NetworkConfiguration { private HttpClientAdapter httpClientAdapter = null; // Optional private String collectorUrl = null; // Required if not specifying a httpClientAdapter - private CookieJar cookieJar = null; // Optional // Getters and Setters @@ -40,14 +38,6 @@ public String getCollectorUrl() { return collectorUrl; } - /** - * Returns the OkHttp CookieJar used for persisting cookies. - * @return CookieJar object - */ - public CookieJar getCookieJar() { - return cookieJar; - } - // Constructors /** @@ -94,16 +84,4 @@ public NetworkConfiguration collectorUrl(String collectorUrl) { this.collectorUrl = collectorUrl; return this; } - - /** - * Adds a custom CookieJar to be used with OkHttpClientAdapters. - * Will be ignored if a custom httpClientAdapter is provided. - * - * @param cookieJar the CookieJar to use - * @return itself - */ - public NetworkConfiguration cookieJar(CookieJar cookieJar) { - this.cookieJar = cookieJar; - return this; - } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index b40d7497..e4da0792 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -29,8 +29,6 @@ import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; -import okhttp3.CookieJar; -import okhttp3.OkHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,7 +75,6 @@ public static abstract class Builder> { private EventStore eventStore = null; // Optional private Map customRetryForStatusCodes = null; // Optional private int threadCount = 50; // Optional - private CookieJar cookieJar = null; // Optional private ScheduledExecutorService requestExecutorService = null; // Optional private EmitterCallback callback = null; // Optional @@ -176,18 +173,6 @@ public T requestExecutorService(final ScheduledExecutorService requestExecutorSe return self(); } - /** - * Adds a custom CookieJar to be used with OkHttpClientAdapters. - * Will be ignored if a custom httpClientAdapter is provided. - * - * @param cookieJar the CookieJar to use - * @return itself - */ - public T cookieJar(final CookieJar cookieJar) { - this.cookieJar = cookieJar; - return self(); - } - /** * Provide a custom EmitterCallback to access successfully sent or failed event payloads. * @@ -201,8 +186,7 @@ public T callback(final EmitterCallback callback) { public BatchEmitter build() { NetworkConfiguration networkConfig = new NetworkConfiguration(collectorUrl) - .httpClientAdapter(httpClientAdapter) - .cookieJar(cookieJar); + .httpClientAdapter(httpClientAdapter); EmitterConfiguration emitterConfig = new EmitterConfiguration() .batchSize(batchSize) @@ -240,8 +224,6 @@ public static Builder builder() { * @param emitterConfig an EmitterConfiguration object */ public BatchEmitter(NetworkConfiguration networkConfig, EmitterConfiguration emitterConfig) { - OkHttpClient client; - // Precondition checks if (emitterConfig.getThreadCount() <= 0) { throw new IllegalArgumentException("threadCount must be greater than 0"); @@ -258,18 +240,7 @@ public BatchEmitter(NetworkConfiguration networkConfig, EmitterConfiguration emi } else { Objects.requireNonNull(networkConfig.getCollectorUrl(), "Collector url must be specified if not using a httpClientAdapter"); - if (networkConfig.getCookieJar() != null) { - client = new OkHttpClient.Builder() - .cookieJar(networkConfig.getCookieJar()) - .build(); - } else { - client = new OkHttpClient.Builder().build(); - } - - httpClientAdapter = OkHttpClientAdapter.builder() // use okhttp as a default - .url(networkConfig.getCollectorUrl()) - .httpClient(client) - .build(); + httpClientAdapter = new OkHttpClientAdapter(networkConfig.getCollectorUrl()); } retryDelay = new AtomicInteger(0); diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index a7492321..925bf70d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -45,6 +45,10 @@ public OkHttpClientAdapter(String url, OkHttpClient httpClient) { this.httpClient = httpClient; } + public OkHttpClientAdapter(String url) { + this(url, new OkHttpClient.Builder().build()); + } + /** * @deprecated Create HttpClientAdapter directly instead * @param Builder diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientWithCookieJarAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientWithCookieJarAdapter.java new file mode 100644 index 00000000..a8d05d70 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientWithCookieJarAdapter.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024-present Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.snowplow.tracker.http; + +// SquareUp +import okhttp3.*; + +/** + * A HttpClient built using OkHttp to send events via GET or POST requests. + * The adapter is configured to use a CollectorCookieJar to store and send cookies set by the collector. + * The cookies are stored in memory. + */ +public class OkHttpClientWithCookieJarAdapter extends OkHttpClientAdapter { + + public OkHttpClientWithCookieJarAdapter(String url) { + super(url, new OkHttpClient.Builder().cookieJar(new CollectorCookieJar()).build()); + } + +} diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 7c2483eb..031e67ce 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -145,13 +145,7 @@ public void testGetWithNullArgument() { @Test public void testRequestWithCookies() throws IOException, InterruptedException { - OkHttpClient httpClient = new OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(1, TimeUnit.SECONDS) - .writeTimeout(1, TimeUnit.SECONDS) - .cookieJar(new CollectorCookieJar()) - .build(); - adapter = new OkHttpClientAdapter(mockWebServer.url("/").toString(), httpClient); + adapter = new OkHttpClientWithCookieJarAdapter(mockWebServer.url("/").toString()); mockWebServer.enqueue(new MockResponse().addHeader("Set-Cookie", "sp=test")); From 037ee683894250b0c16389ab152c21fe2f6efe13 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 11 Jan 2024 10:54:45 +0100 Subject: [PATCH 123/128] Remove deprecated APIs (close #373) PR #374 --- .../snowplow/tracker/Subject.java | 158 ------------------ .../snowplow/tracker/Tracker.java | 81 --------- .../snowplow/tracker/constants/Parameter.java | 3 - .../tracker/emitter/BatchEmitter.java | 157 ----------------- .../http/AbstractHttpClientAdapter.java | 52 ------ .../tracker/http/ApacheHttpClientAdapter.java | 53 ------ .../tracker/http/OkHttpClientAdapter.java | 53 ------ .../snowplow/tracker/TrackerTest.java | 1 - .../emitter/BatchEmitterBuilderTest.java | 8 +- 9 files changed, 5 insertions(+), 561 deletions(-) diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java index 011e4a9a..bba82a15 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Subject.java @@ -63,164 +63,6 @@ public Subject(Subject subject){ standardPairs.putAll(subject.getSubject()); } - /** - * @deprecated Use SubjectConfiguration class instead - * @return a SubjectBuilder object - */ - @Deprecated - public static SubjectBuilder builder() { - return new SubjectBuilder(); - } - - /** - * Builder for the Subject - * @deprecated Use SubjectConfiguration class instead - */ - @Deprecated - public static class SubjectBuilder { - - private String userId; // Optional - private int screenResWidth = 0; // Optional - private int screenResHeight = 0; // Optional - private int viewPortWidth = 0; // Optional - private int viewPortHeight = 0; // Optional - private int colorDepth = 0; // Optional - private String timezone = Utils.getTimezone(); // Optional - private String language; // Optional - private String ipAddress; // Optional - private String useragent; // Optional - private String networkUserId; // Optional - private String domainUserId; // Optional - private String domainSessionId; // Optional - - /** - * @param userId a user id string - * @return itself - */ - public SubjectBuilder userId(String userId) { - this.userId = userId; - return this; - } - - /** - * @param width a width integer - * @param height a height integer - * @return itself - */ - public SubjectBuilder screenResolution(int width, int height) { - this.screenResWidth = width; - this.screenResHeight = height; - return this; - } - - /** - * @param width a width integer - * @param height a height integer - * @return itself - */ - public SubjectBuilder viewPort(int width, int height) { - this.viewPortWidth = width; - this.viewPortHeight = height; - return this; - } - - /** - * @param depth a color depth integer - * @return itself - */ - public SubjectBuilder colorDepth(int depth) { - this.colorDepth = depth; - return this; - } - - /** - * Note that timezone is set by default to the server's timezone - * (`TimeZone tz = Calendar.getInstance().getTimeZone().getID()`) - * @param timezone a timezone string - * @return itself - */ - public SubjectBuilder timezone(String timezone) { - this.timezone = timezone; - return this; - } - - /** - * @param language a language string - * @return itself - */ - public SubjectBuilder language(String language) { - this.language = language; - return this; - } - - /** - * @param ipAddress a ipAddress string - * @return itself - */ - public SubjectBuilder ipAddress(String ipAddress) { - this.ipAddress = ipAddress; - return this; - } - - /** - * @param useragent a useragent string - * @return itself - */ - public SubjectBuilder useragent(String useragent) { - this.useragent = useragent; - return this; - } - - /** - * @param networkUserId a networkUserId string - * @return itself - */ - public SubjectBuilder networkUserId(String networkUserId) { - this.networkUserId = networkUserId; - return this; - } - - /** - * @param domainUserId a domainUserId string - * @return itself - */ - public SubjectBuilder domainUserId(String domainUserId) { - this.domainUserId = domainUserId; - return this; - } - - /** - * @param domainSessionId a domainSessionId string - * @return itself - */ - public SubjectBuilder domainSessionId(String domainSessionId) { - this.domainSessionId = domainSessionId; - return this; - } - - /** - * Creates a new Subject - * - * @return a new Subject object - */ - public Subject build() { - SubjectConfiguration subjectConfig = new SubjectConfiguration() - .userId(userId) - .screenResolution(screenResWidth, screenResHeight) - .viewPort(viewPortWidth, viewPortHeight) - .colorDepth(colorDepth) - .timezone(timezone) - .language(language) - .ipAddress(ipAddress) - .useragent(useragent) - .networkUserId(networkUserId) - .domainUserId(domainUserId) - .domainSessionId(domainSessionId); - - return new Subject(subjectConfig); - } - } - /** * Sets the User ID * diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java index da2a107f..8a75e367 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Tracker.java @@ -70,87 +70,6 @@ public Tracker(TrackerConfiguration trackerConfig, Emitter emitter) { this(trackerConfig, emitter, null); } - /** - * Builder for the Tracker - * @deprecated Use TrackerConfiguration class instead - */ - @Deprecated - public static class TrackerBuilder { - - private final Emitter emitter; // Required - private final String namespace; // Required - private final String appId; // Required - private Subject subject = null; // Optional - private DevicePlatform platform = DevicePlatform.ServerSideApp; // Optional - private boolean base64Encoded = true; // Optional - - /** - * @param emitter Emitter to which events will be sent - * @param namespace Identifier for the Tracker instance - * @param appId Application ID - */ - public TrackerBuilder(Emitter emitter, String namespace, String appId) { - this.emitter = emitter; - this.namespace = namespace; - this.appId = appId; - } - - /** - * @param subject Subject to be tracked - * @return itself - */ - public TrackerBuilder subject(Subject subject) { - this.subject = subject; - return this; - } - - /** - * The {@link DevicePlatform} the tracker is running on (default is "srv", ServerSideApp). - * - * @param platform The device platform the tracker is running on - * @return itself - */ - public TrackerBuilder platform(DevicePlatform platform) { - this.platform = platform; - return this; - } - - /** - * Whether JSONs in the payload should be base-64 encoded (default is true) - * - * @param base64 JSONs should be encoded or not - * @return itself - */ - public TrackerBuilder base64(Boolean base64) { - this.base64Encoded = base64; - return this; - } - - /** - * Creates a new Tracker - * - * @return a new Tracker object - */ - public Tracker build() { - TrackerConfiguration trackerConfig = new TrackerConfiguration(namespace, appId) - .platform(platform) - .base64Encoded(base64Encoded); - return new Tracker(trackerConfig, emitter, subject); - } - } - - /** - * @deprecated Use TrackerConfiguration class instead - * @param emitter Emitter object - * @param namespace unique tracker namespace - * @param appId application ID - * @return TrackerBuilder object - */ - @Deprecated - public static TrackerBuilder builder(Emitter emitter, String namespace, String appId) { - return new TrackerBuilder(emitter, namespace, appId); - } - // --- Setters /** diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java index c4b9f38d..19ec1863 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/constants/Parameter.java @@ -29,9 +29,6 @@ public class Parameter { public static final String DEVICE_CREATED_TIMESTAMP = "dtm"; public static final String DEVICE_SENT_TIMESTAMP = "stm"; - /** deprecated Indicate the specific timestamp to use. This is kept for compatibility with older versions. */ - @Deprecated - public static final String TIMESTAMP = DEVICE_CREATED_TIMESTAMP; public static final String TRACKER_VERSION = "tv"; public static final String APP_ID = "aid"; public static final String NAMESPACE = "tna"; diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java index e4da0792..9c2e7a13 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitter.java @@ -60,163 +60,6 @@ public class BatchEmitter implements Emitter, Closeable { private final Map customRetryForStatusCodes; private final EmitterCallback callback; - /** - * @deprecated Use NetworkConfiguration/EmitterConfiguration classes instead - * @param Builder - */ - @Deprecated - public static abstract class Builder> { - protected abstract T self(); - - private HttpClientAdapter httpClientAdapter; // Optional - private String collectorUrl = null; // Required if not specifying a httpClientAdapter - private int batchSize = 50; // Optional - private int bufferCapacity = 10000; - private EventStore eventStore = null; // Optional - private Map customRetryForStatusCodes = null; // Optional - private int threadCount = 50; // Optional - private ScheduledExecutorService requestExecutorService = null; // Optional - private EmitterCallback callback = null; // Optional - - /** - * Adds a custom HttpClientAdapter to the Emitter (default is OkHttpClientAdapter). - * - * @param httpClientAdapter the adapter to use - * @return itself - */ - public T httpClientAdapter(final HttpClientAdapter httpClientAdapter) { - this.httpClientAdapter = httpClientAdapter; - return self(); - } - - - /** - * Sets the emitter url for when a httpClientAdapter is not specified. - * It will be used to create the default OkHttpClientAdapter. - * - * @param collectorUrl the url for the default httpClientAdapter - * @return itself - */ - public T url(final String collectorUrl) { - this.collectorUrl = collectorUrl; - return self(); - } - - /** - * The default batch size is 50. - * - * @param batchSize The count of events to send in one HTTP request - * @return itself - */ - public T batchSize(final int batchSize) { - this.batchSize = batchSize; - return self(); - } - - /** - * The default buffer capacity is 10 000 events. - * When the buffer is full (due to network outage), new events are lost. - * - * @param bufferCapacity The maximum capacity of the default InMemoryEventStore event buffer - * @return itself - */ - public T bufferCapacity(final int bufferCapacity) { - this.bufferCapacity = bufferCapacity; - return self(); - } - - /** - * The default EventStore is InMemoryEventStore. - * - * @param eventStore The EventStore to use - * @return itself - */ - public T eventStore(final EventStore eventStore) { - this.eventStore = eventStore; - return self(); - } - - /** - * Set custom retry rules for HTTP status codes received in emit responses from the Collector. - * By default, retry will not occur for status codes 400, 401, 403, 410 or 422. This can be overridden here. - * Note that 2xx codes will never retry as they are considered successful. - * @param customRetryForStatusCodes Mapping of integers (status codes) to booleans (true for retry and false for not retry) - * @return itself - */ - public T customRetryForStatusCodes(Map customRetryForStatusCodes) { - this.customRetryForStatusCodes = customRetryForStatusCodes; - return self(); - } - - /** - * Sets the Thread Count for the ScheduledExecutorService (default is 50). - * - * @param threadCount the size of the thread pool - * @return itself - */ - public T threadCount(final int threadCount) { - this.threadCount = threadCount; - return self(); - } - - /** - * Set a custom ScheduledExecutorService to send http requests (default is ScheduledThreadPoolExecutor). - *

- * Implementation note: Be aware that calling `close()` on a BatchEmitter instance - * has a side-effect and will shutdown that ExecutorService. - * - * @param requestExecutorService the ScheduledExecutorService to use - * @return itself - */ - public T requestExecutorService(final ScheduledExecutorService requestExecutorService) { - this.requestExecutorService = requestExecutorService; - return self(); - } - - /** - * Provide a custom EmitterCallback to access successfully sent or failed event payloads. - * - * @param callback an EmitterCallback - * @return itself - */ - public T callback(final EmitterCallback callback) { - this.callback = callback; - return self(); - } - - public BatchEmitter build() { - NetworkConfiguration networkConfig = new NetworkConfiguration(collectorUrl) - .httpClientAdapter(httpClientAdapter); - - EmitterConfiguration emitterConfig = new EmitterConfiguration() - .batchSize(batchSize) - .bufferCapacity(bufferCapacity) - .eventStore(eventStore) - .customRetryForStatusCodes(customRetryForStatusCodes) - .threadCount(threadCount) - .requestExecutorService(requestExecutorService) - .callback(callback); - - return new BatchEmitter(networkConfig, emitterConfig); - } - } - - private static class Builder2 extends Builder { - @Override - protected Builder2 self() { - return this; - } - } - - /** - * @deprecated Use NetworkConfiguration/EmitterConfiguration classes instead - * @return Builder object - */ - @Deprecated - public static Builder builder() { - return new Builder2(); - } - /** * Creates a BatchEmitter object from configuration objects. * diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java index bcda41f0..58f586f6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -28,58 +28,6 @@ public AbstractHttpClientAdapter(String url) { this.url = url.replaceFirst("/*$", ""); } - /** - * @deprecated Create HttpClientAdapter directly instead - * @param Builder - */ - @Deprecated - public static abstract class Builder> { - - private String url; // Required - protected abstract T self(); - - /** - * Adds a URI to the Client Adapter - * - * @param url the emitter url - * @return itself - */ - public T url(String url) { - this.url = url.replaceFirst("/*$", ""); - return self(); - } - } - - private static class Builder2 extends Builder { - @Override - protected Builder2 self() { - return this; - } - } - - /** - * @deprecated Create HttpClientAdapter directly instead - * @return Builder object - */ - @Deprecated - public static Builder builder() { - return new Builder2(); - } - - /** - * @deprecated Create HttpClientAdapter directly instead - * @param builder Builder object - */ - @Deprecated - protected AbstractHttpClientAdapter(Builder builder) { - // Precondition checks - if (!Utils.isValidUrl(builder.url)) { - throw new IllegalArgumentException(); - } - - this.url = builder.url; - } - /** * Returns the HttpClient URI * diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java index 56099767..de72604c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -43,59 +43,6 @@ public ApacheHttpClientAdapter(String url, CloseableHttpClient httpClient) { this.httpClient = httpClient; } - /** - * @deprecated Create HttpClientAdapter directly instead - * @param Builder - */ - @Deprecated - public static abstract class Builder> extends AbstractHttpClientAdapter.Builder { - - private CloseableHttpClient httpClient; // Required - - /** - * @param httpClient The Apache HTTP Client to use - * @return itself - */ - public T httpClient(CloseableHttpClient httpClient) { - this.httpClient = httpClient; - return self(); - } - - public ApacheHttpClientAdapter build() { - return new ApacheHttpClientAdapter(this); - } - } - - private static class Builder2 extends Builder { - @Override - protected Builder2 self() { - return this; - } - } - - /** - * @deprecated Create HttpClientAdapter directly instead - * @return Builder object - */ - @Deprecated - public static Builder builder() { - return new Builder2(); - } - - /** - * @deprecated Create HttpClientAdapter directly instead - * @param builder Builder object - */ - @Deprecated - protected ApacheHttpClientAdapter(Builder builder) { - super(builder); - - // Precondition checks - Objects.requireNonNull(builder.httpClient); - - this.httpClient = builder.httpClient; - } - /** * Returns the HttpClient in use; it is up to the developer * to cast it back to its original class. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java index 925bf70d..95df941b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -49,59 +49,6 @@ public OkHttpClientAdapter(String url) { this(url, new OkHttpClient.Builder().build()); } - /** - * @deprecated Create HttpClientAdapter directly instead - * @param Builder - */ - @Deprecated - public static abstract class Builder> extends AbstractHttpClientAdapter.Builder { - - private OkHttpClient httpClient; // Required - - /** - * @param httpClient The OkHTTP Client to use - * @return itself - */ - public T httpClient(OkHttpClient httpClient) { - this.httpClient = httpClient; - return self(); - } - - public OkHttpClientAdapter build() { - return new OkHttpClientAdapter(this); - } - } - - private static class Builder2 extends Builder { - @Override - protected Builder2 self() { - return this; - } - } - - /** - * @deprecated Create HttpClientAdapter directly instead - * @return Builder object - */ - @Deprecated - public static Builder builder() { - return new Builder2(); - } - - /** - * @deprecated Create HttpClientAdapter directly instead - * @param builder Builder object - */ - @Deprecated - protected OkHttpClientAdapter(Builder builder) { - super(builder); - - // Precondition checks - Objects.requireNonNull(builder.httpClient); - - httpClient = builder.httpClient; - } - /** * Returns the HttpClient in use; it is up to the developer * to cast it back to its original class. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 73ee9a7b..d35a2b60 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -15,7 +15,6 @@ import java.util.*; import static java.util.Collections.singletonList; -import com.snowplowanalytics.snowplow.tracker.configuration.EmitterConfiguration; import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; import com.snowplowanalytics.snowplow.tracker.configuration.TrackerConfiguration; import com.snowplowanalytics.snowplow.tracker.emitter.BatchEmitter; diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java index 8013bd37..dc9efb63 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -14,6 +14,7 @@ import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload; +import com.snowplowanalytics.snowplow.tracker.configuration.NetworkConfiguration; import com.snowplowanalytics.snowplow.tracker.http.HttpClientAdapter; import org.junit.Assert; import org.junit.Test; @@ -22,13 +23,14 @@ public class BatchEmitterBuilderTest { @Test public void setNeitherHttpClientAdapterOrCollectorUrl_shouldThrowException() { - Exception exception = Assert.assertThrows(Exception.class, () -> BatchEmitter.builder().build()); + String collectorUrl = null; + Exception exception = Assert.assertThrows(Exception.class, () -> new BatchEmitter(new NetworkConfiguration(collectorUrl))); Assert.assertEquals("Collector url must be specified if not using a httpClientAdapter", exception.getMessage()); } @Test public void setCollectorUrlAndNoHttpClientAdapter_shouldInitialiseCorrectly() { - BatchEmitter emitter = BatchEmitter.builder().url("https://mycollector.com").build(); + BatchEmitter emitter = new BatchEmitter(new NetworkConfiguration("https://mycollector.com")); Assert.assertNotNull(emitter); } @@ -56,7 +58,7 @@ public Object getHttpClient() { } }; - BatchEmitter emitter = BatchEmitter.builder().httpClientAdapter(mockHttpClientAdapter).build(); + BatchEmitter emitter = new BatchEmitter(new NetworkConfiguration(mockHttpClientAdapter)); Assert.assertNotNull(emitter); } } From cd0f019a486fb44e928752ff23c9f92544cc4883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Fri, 12 Jan 2024 10:38:01 +0100 Subject: [PATCH 124/128] Prepare for 2.0.0 release --- CHANGELOG | 14 ++++++++++++++ build.gradle | 2 +- examples/simple-console/build.gradle | 4 ++-- .../src/main/java/com/snowplowanalytics/Main.java | 4 ++-- .../snowplow/tracker/TrackerTest.java | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b62404a5..d55c6742 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,17 @@ +Java 2.0.0 (2024-01-12) +----------------------- +Add builder methods Subject to allow method chaining (#303) +Add okhttp adapter with cookie jar and remove cookie jar from network configuration (#361) +Remove deprecated APIs (#373) +Update to Apache Http Client to v5 (#364) +Upgrade okhttp dependency to version 4.12 (#365) +Bump slf4j-simple and slf4j-api to 2.0.11 +Bump junit-jupiter-api to 5.10.1 +Bump jackson-databind to 2.16.1 +Bump commons-net to 3.10 +Bump commons-codec to 1.16 +Update copyright headers in source files (#375) + Java 1.0.1 (2023-11-06) ----------------------- Fix Issue with OkHttpClientAdapter (#366) (thanks to @eusorov for the contribution!) diff --git a/build.gradle b/build.gradle index ab621a69..e6a84507 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '1.0.1' +version = '2.0.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/examples/simple-console/build.gradle b/examples/simple-console/build.gradle index 78d31acc..0b134e77 100644 --- a/examples/simple-console/build.gradle +++ b/examples/simple-console/build.gradle @@ -16,9 +16,9 @@ test { } dependencies { - implementation 'com.snowplowanalytics:snowplow-java-tracker:1.+' + implementation 'com.snowplowanalytics:snowplow-java-tracker:2.+' - implementation ('com.snowplowanalytics:snowplow-java-tracker:1.+') { + implementation ('com.snowplowanalytics:snowplow-java-tracker:2.+') { capabilities { requireCapability 'com.snowplowanalytics:snowplow-java-tracker-okhttp-support' } diff --git a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java index d1085661..e0f59265 100644 --- a/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java +++ b/examples/simple-console/src/main/java/com/snowplowanalytics/Main.java @@ -86,7 +86,7 @@ public static void main(String[] args) throws InterruptedException { .quantity(2) .name("name") .category("category") - .currency("currency") + .currency("EUR") .customContext(context) .build(); @@ -100,7 +100,7 @@ public static void main(String[] args) throws InterruptedException { .city("city") .state("state") .country("country") - .currency("currency") + .currency("EUR") .items(item) // EcommerceTransactionItem events are added to a parent EcommerceTransaction here .customContext(context) .build(); diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index d35a2b60..d6733765 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -576,7 +576,7 @@ public void testCreateWithConfiguration() { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "an-app-id"), mockEmitter); - assertEquals("java-1.0.1", tracker.getTrackerVersion()); + assertEquals("java-2.0.0", tracker.getTrackerVersion()); } @Test From 26655a9e9d26967c19459fd6e5b06895265e60b1 Mon Sep 17 00:00:00 2001 From: Stephen Murby Date: Tue, 13 Feb 2024 09:14:08 +0000 Subject: [PATCH 125/128] Add support for serializing DateTime in self-describing data (close #378) PR #379 --------- Co-authored-by: Matus Tomlein --- build.gradle | 1 + .../com/snowplowanalytics/snowplow/tracker/Utils.java | 7 ++++++- .../snowplowanalytics/snowplow/tracker/UtilsTest.java | 10 ++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e6a84507..03267c77 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,7 @@ dependencies { // Jackson JSON processor api 'com.fasterxml.jackson.core:jackson-databind:2.16.1' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1' // Testing libraries testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1' diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java index 5e3c936a..af7e94a3 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/Utils.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,7 +31,10 @@ public class Utils { private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); - private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectMapper objectMapper + = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // Tracker Utils diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index e74ea851..24ba8e44 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -17,6 +17,8 @@ // Java import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.Map; @@ -68,6 +70,14 @@ public void testMapToJSONString() { Map payload2 = new LinkedHashMap<>(); payload2.put("k1", new Object()); assertEquals("", Utils.mapToJSONString(payload2)); + + Map payload3 = new LinkedHashMap<>(); + payload3.put("k1", LocalDateTime.of(2020, 1, 1, 0, 0)); + assertEquals("{\"k1\":\"2020-01-01T00:00:00\"}", Utils.mapToJSONString(payload3)); + + Map payload4 = new LinkedHashMap<>(); + payload4.put("k1", LocalDate.of(2020, 1, 1)); + assertEquals("{\"k1\":\"2020-01-01\"}", Utils.mapToJSONString(payload4)); } @Test From edc2d2378841d6ae7e7ac5efc6800fd58894b3f6 Mon Sep 17 00:00:00 2001 From: Stephen Murby Date: Wed, 14 Feb 2024 15:10:38 +0000 Subject: [PATCH 126/128] Add equality functions for SelfDescribing and SelfDescribingJson so that they can be compared in unit tests (close #380) PR #381 --- .../tracker/events/SelfDescribing.java | 18 ++++++ .../tracker/payload/SelfDescribingJson.java | 15 +++++ .../tracker/events/SelfDescribingTest.java | 37 +++++++++++ .../payload/SelfDescribingJsonTest.java | 63 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 src/test/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribingTest.java diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java index 5ff1dc61..b515b7e6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java @@ -95,4 +95,22 @@ public TrackerPayload getPayload() { Parameter.SELF_DESCRIBING_ENCODED, Parameter.SELF_DESCRIBING); return putTrueTimestamp(payload); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SelfDescribing that = (SelfDescribing) o; + + if (base64Encode != that.base64Encode) return false; + return Objects.equals(eventData, that.eventData); + } + + @Override + public int hashCode() { + int result = eventData != null ? eventData.hashCode() : 0; + result = 31 * result + (base64Encode ? 1 : 0); + return result; + } } diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java index c2c17ac0..915c17d8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -198,4 +198,19 @@ public long getByteSize() { public String toString() { return Utils.mapToJSONString(payload); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SelfDescribingJson that = (SelfDescribingJson) o; + + return payload.equals(that.payload); + } + + @Override + public int hashCode() { + return payload.hashCode(); + } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribingTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribingTest.java new file mode 100644 index 00000000..01e21e61 --- /dev/null +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribingTest.java @@ -0,0 +1,37 @@ +package com.snowplowanalytics.snowplow.tracker.events; + +import com.snowplowanalytics.snowplow.tracker.payload.SelfDescribingJson; + +// JUnit +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class SelfDescribingTest { + + @Test + public void testEqualityOfTwoInstances() { + SelfDescribing.Builder builder = SelfDescribing.builder() + .eventData(new SelfDescribingJson("schema-name")); + + SelfDescribing a = new SelfDescribing( builder ); + SelfDescribing b = new SelfDescribing( builder ); + + assertEquals(a, b); + } + + @Test + public void testNegativeEqualityOfTwoInstances() { + SelfDescribing.Builder builderOne = SelfDescribing.builder() + .eventData(new SelfDescribingJson("schema-name-one")); + + SelfDescribing.Builder builderTwo = SelfDescribing.builder() + .eventData(new SelfDescribingJson("schema-name-two")); + + SelfDescribing a = new SelfDescribing( builderOne ); + SelfDescribing b = new SelfDescribing( builderTwo ); + + assertNotEquals(a, b); + } + +} \ No newline at end of file diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java index 8cfdd414..68f144e1 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java @@ -20,6 +20,9 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotEquals; + + public class SelfDescribingJsonTest { @@ -67,4 +70,64 @@ public void testMakeSdjWithSdj() { assertNotNull(sdj); assertEquals(expected, sdjString); } + + @Test + public void testEqualityOfTwoInstances_withSchemaNameOnly() { + SelfDescribingJson a = new SelfDescribingJson("schema"); + SelfDescribingJson b = new SelfDescribingJson("schema"); + assertEquals(a, b); + } + + @Test + public void testEqualityOfTwoInstances_withTrackerPayload() { + TrackerPayload nestedData = new TrackerPayload(); + nestedData.add("key", "value"); + SelfDescribingJson a = new SelfDescribingJson("schema", nestedData); + SelfDescribingJson b = new SelfDescribingJson("schema", nestedData); + assertEquals(a, b); + } + + @Test + public void testEqualityOfTwoInstances_withNestedEvent() { + TrackerPayload nestedData = new TrackerPayload(); + nestedData.add("key", "value"); + SelfDescribingJson nestedEvent = new SelfDescribingJson("nested_event", nestedData); + SelfDescribingJson a = new SelfDescribingJson("schema", nestedEvent); + SelfDescribingJson b = new SelfDescribingJson("schema", nestedEvent); + assertEquals(a, b); + } + + @Test + public void testNegativeEqualityOfTwoInstances_withSchemaNameOnly() { + SelfDescribingJson a = new SelfDescribingJson("schema-one"); + SelfDescribingJson b = new SelfDescribingJson("schema-two"); + assertNotEquals(a, b); + } + + @Test + public void testNegativeEqualityOfTwoInstances_withTrackerPayload() { + TrackerPayload nestedDataOne = new TrackerPayload(); + nestedDataOne.add("key", "value-one"); + TrackerPayload nestedDataTwo = new TrackerPayload(); + nestedDataTwo.add("key", "value-two"); + SelfDescribingJson a = new SelfDescribingJson("schema", nestedDataOne); + SelfDescribingJson b = new SelfDescribingJson("schema", nestedDataTwo); + assertNotEquals(a, b); + } + + @Test + public void testNegativeEqualityOfTwoInstances_withNestedEvent() { + TrackerPayload nestedDataOne = new TrackerPayload(); + nestedDataOne.add("key", "value-one"); + SelfDescribingJson nestedEventOne = new SelfDescribingJson("nested_event", nestedDataOne); + + TrackerPayload nestedDataTwo = new TrackerPayload(); + nestedDataTwo.add("key", "value-two"); + SelfDescribingJson nestedEventTwo = new SelfDescribingJson("nested_event", nestedDataTwo); + + + SelfDescribingJson a = new SelfDescribingJson("schema", nestedEventOne); + SelfDescribingJson b = new SelfDescribingJson("schema", nestedEventTwo); + assertNotEquals(a, b); + } } From b9636fd0caf83ae495cef6fe9bf05c75b1a95f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 14 Feb 2024 18:52:02 +0100 Subject: [PATCH 127/128] Prepare for 2.1.0 release --- CHANGELOG | 5 +++++ build.gradle | 2 +- .../com/snowplowanalytics/snowplow/tracker/TrackerTest.java | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d55c6742..c750412a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +Java 2.1.0 (2024-02-14) +----------------------- +Add support for serializing DateTime in self-describing data (#378) (thanks to @stephen-murby for the contribution!) +Add equality functions for SelfDescribing and SelfDescribingJson so that they can be compared in unit tests (#380) (thanks to @stephen-murby for the contribution!) + Java 2.0.0 (2024-01-12) ----------------------- Add builder methods Subject to allow method chaining (#303) diff --git a/build.gradle b/build.gradle index 03267c77..95b21d8e 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ wrapper.gradleVersion = '6.5.0' group = 'com.snowplowanalytics' archivesBaseName = 'snowplow-java-tracker' -version = '2.0.0' +version = '2.1.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index d6733765..c2bbbd36 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -576,7 +576,7 @@ public void testCreateWithConfiguration() { @Test public void testGetTrackerVersion() { Tracker tracker = new Tracker(new TrackerConfiguration("namespace", "an-app-id"), mockEmitter); - assertEquals("java-2.0.0", tracker.getTrackerVersion()); + assertEquals("java-2.1.0", tracker.getTrackerVersion()); } @Test From 0741c102934a2fd330754546ccf33bc07703aed2 Mon Sep 17 00:00:00 2001 From: Patricio Date: Wed, 5 Nov 2025 16:08:16 +0100 Subject: [PATCH 128/128] claude mds instrumentation (#384) --- CLAUDE.md | 370 +++++++++++++++++ .../snowplow/tracker/emitter/CLAUDE.md | 350 +++++++++++++++++ .../snowplow/tracker/events/CLAUDE.md | 293 ++++++++++++++ src/test/java/CLAUDE.md | 371 ++++++++++++++++++ 4 files changed, 1384 insertions(+) create mode 100644 CLAUDE.md create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/CLAUDE.md create mode 100644 src/main/java/com/snowplowanalytics/snowplow/tracker/events/CLAUDE.md create mode 100644 src/test/java/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..da02c7c4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,370 @@ +# Snowplow Java Tracker - Architecture & Development Guide + +## Project Overview + +The Snowplow Java Tracker is a library for tracking analytics events and sending them to Snowplow collectors. It provides a robust, configurable event tracking system for Java applications (JDK 8+) with support for batch processing, retry logic, and multiple HTTP client implementations. + +**Key Technologies:** +- Java 8+ (minimum requirement) +- Gradle 6.5.0 build system +- Jackson for JSON processing +- OkHttp/Apache HTTP clients (optional features) +- JUnit 4/5 for testing +- SLF4J for logging + +## Development Commands + +```bash +# Build and run tests +./gradlew build + +# Run tests only +./gradlew test + +# Publish to local Maven repository +./gradlew publishToMavenLocal + +# Generate version info +./gradlew generateSources + +# Clean build artifacts +./gradlew clean + +# Run example application +cd examples/simple-console +./gradlew jar +java -jar ./build/libs/simple-console-all-0.0.1.jar "http://collector-url" +``` + +## Architecture + +### Core Components + +1. **Tracker**: Central component that processes and sends events +2. **Emitter**: Handles HTTP communication and batch processing +3. **Events**: Type-safe event models (PageView, Structured, SelfDescribing, etc.) +4. **Subject**: User/device information attached to events +5. **Payload**: Event data structures and serialization +6. **Configuration**: Builder-pattern configuration objects + +### Layer Organization + +``` +com.snowplowanalytics.snowplow.tracker/ +├── configuration/ # Configuration builders +├── emitter/ # Event transmission layer +├── events/ # Event type definitions +├── payload/ # Data structures & serialization +├── http/ # HTTP client adapters +└── constants/ # Constants and parameters +``` + +## Core Architectural Principles + +### 1. Configuration-First Design +All components use configuration objects with fluent builders: +```java +// ✅ Use configuration objects +TrackerConfiguration config = new TrackerConfiguration(namespace, appId) + .platform(DevicePlatform.ServerSideApp) + .base64Encoded(true); + +// ❌ Don't use complex constructors +new Tracker(namespace, appId, platform, base64, emitter, subject); +``` + +### 2. Builder Pattern for Events +All events use the builder pattern for construction: +```java +// ✅ Use builders for events +PageView event = PageView.builder() + .pageUrl("https://example.com") + .customContext(contexts) + .build(); + +// ❌ Don't use constructors or setters +PageView event = new PageView(); +event.setPageUrl("https://example.com"); +``` + +### 3. Immutable Event Data +Events are immutable after creation: +```java +// ✅ Create complete events +Structured event = Structured.builder() + .category("category") + .action("action") + .build(); + +// ❌ Don't modify after creation +event.setCategory("new-category"); // No such method +``` + +### 4. Nullable Pattern with Validation +Required fields are validated, optional fields can be null: +```java +// ✅ Validate required fields +Objects.requireNonNull(namespace); +if (namespace.isEmpty()) { + throw new IllegalArgumentException("namespace cannot be empty"); +} + +// ✅ Optional fields can be null +private Subject subject; // Can be null +``` + +## Critical Import Patterns + +### Standard Package Organization +```java +// ✅ Correct import order +// 1. Java standard library +import java.util.*; +import java.io.Closeable; + +// 2. Third-party libraries +import org.slf4j.Logger; +import com.fasterxml.jackson.databind.ObjectMapper; + +// 3. Snowplow tracker packages +import com.snowplowanalytics.snowplow.tracker.*; +import com.snowplowanalytics.snowplow.tracker.events.*; +``` + +## Essential Library Patterns + +### 1. Tracker Creation Pattern +```java +// ✅ Use Snowplow factory with configurations +Tracker tracker = Snowplow.createTracker( + trackerConfig, + networkConfig, + emitterConfig, + subjectConfig +); + +// ❌ Don't create tracker directly +Tracker tracker = new Tracker(config, emitter); +``` + +### 2. Event Tracking Pattern +```java +// ✅ Track returns event IDs +List eventIds = tracker.track(event); + +// ✅ Handle batch events (EcommerceTransaction) +List ids = tracker.track(transaction); // May return multiple IDs +``` + +### 3. SelfDescribingJson Pattern +```java +// ✅ Use schema + data constructor +SelfDescribingJson context = new SelfDescribingJson( + "iglu:com.example/context/jsonschema/1-0-0", + Collections.singletonMap("key", "value") +); + +// ❌ Don't use TrackerPayload as data +new SelfDescribingJson(schema, new TrackerPayload()); // Contains unwanted eid/dtm +``` + +### 4. HTTP Client Adapter Pattern +```java +// ✅ Let configuration choose adapter +BatchEmitter emitter = new BatchEmitter(networkConfig, emitterConfig); + +// ✅ Or provide custom adapter +HttpClientAdapter adapter = new OkHttpClientAdapter(url); +networkConfig.httpClientAdapter(adapter); +``` + +## Model Organization Pattern + +### Event Hierarchy +``` +Event (interface) +└── AbstractEvent (base class) + ├── PageView + ├── Structured + ├── SelfDescribing + ├── ScreenView + ├── Timing + ├── EcommerceTransaction + └── EcommerceTransactionItem +``` + +### Payload Types +``` +Payload (interface) +├── TrackerPayload (main event payload) +├── SelfDescribingJson (schema-based data) +└── BatchPayload (POST request wrapper) +``` + +## Common Pitfalls & Solutions + +### 1. TrackerPayload in SelfDescribingJson +```java +// ❌ Wrong: TrackerPayload adds unwanted eid/dtm +SelfDescribingJson data = new SelfDescribingJson( + schema, + new TrackerPayload() +); + +// ✅ Correct: Use Map or Object +SelfDescribingJson data = new SelfDescribingJson( + schema, + new HashMap() +); +``` + +### 2. Synchronous Event Sending +```java +// ❌ Wrong: Expecting immediate send +tracker.track(event); +// Event not sent yet! + +// ✅ Correct: Events are batched +tracker.track(event); +tracker.getEmitter().flushBuffer(); // Force send +tracker.close(); // Or close to flush +``` + +### 3. Missing Required Configuration +```java +// ❌ Wrong: Missing collector URL +NetworkConfiguration network = new NetworkConfiguration(); + +// ✅ Correct: Provide URL or adapter +NetworkConfiguration network = new NetworkConfiguration("https://collector.example.com"); +``` + +### 4. Thread Safety +```java +// ❌ Wrong: Sharing Subject across threads +Subject shared = new Subject(); +// Multiple threads modifying shared + +// ✅ Correct: Event-specific subjects +PageView.builder() + .subject(new Subject()) // Thread-local + .build(); +``` + +## File Structure Template + +``` +snowplow-java-tracker/ +├── build.gradle # Main build configuration +├── src/ +│ ├── main/java/com/snowplowanalytics/snowplow/tracker/ +│ │ ├── Tracker.java # Core tracker +│ │ ├── Snowplow.java # Factory & registry +│ │ ├── Subject.java # User/device info +│ │ ├── configuration/ # Config objects +│ │ ├── emitter/ # Event transmission +│ │ ├── events/ # Event types +│ │ ├── payload/ # Data structures +│ │ └── http/ # HTTP adapters +│ └── test/java/ # Unit tests +└── examples/ + └── simple-console/ # Usage example +``` + +## Testing Patterns + +### 1. Mock Emitter Pattern +```java +// ✅ Use MockEmitter for testing +class MockEmitter implements Emitter { + public List eventList = new ArrayList<>(); + + @Override + public boolean add(TrackerPayload payload) { + eventList.add(payload); + return true; + } +} +``` + +### 2. MockWebServer for HTTP Tests +```java +// ✅ Use OkHttp MockWebServer +MockWebServer server = new MockWebServer(); +server.enqueue(new MockResponse().setResponseCode(200)); +String url = server.url("/").toString(); +``` + +### 3. Test Event Creation +```java +// ✅ Test with all optional fields +PageView event = PageView.builder() + .pageUrl("https://example.com") + .customContext(contexts) + .trueTimestamp(timestamp) + .subject(subject) + .build(); +``` + +## Quick Reference + +### Event Types Checklist +- [ ] **PageView**: Web page views +- [ ] **Structured**: Category/action events +- [ ] **SelfDescribing**: Custom schema-based events +- [ ] **ScreenView**: Mobile screen views +- [ ] **Timing**: Performance timing +- [ ] **EcommerceTransaction**: Purchase events (deprecated) + +### Configuration Components +- [ ] **TrackerConfiguration**: namespace, appId, platform +- [ ] **NetworkConfiguration**: collector URL, HTTP client +- [ ] **EmitterConfiguration**: batch size, thread count, callbacks +- [ ] **SubjectConfiguration**: user ID, session, device info + +### Snowplow Concepts +- **Collector**: Endpoint receiving events +- **Schema**: Iglu schema for self-describing data +- **Context**: Additional data attached to events +- **Subject**: User/device information +- **Namespace**: Unique tracker identifier + +## Contributing to CLAUDE.md + +When adding or updating content in this document, please follow these guidelines: + +### File Size Limit +- **CLAUDE.md must not exceed 40KB** (currently ~19KB) +- Check file size after updates: `wc -c CLAUDE.md` +- Remove outdated content if approaching the limit + +### Code Examples +- Keep all code examples **4 lines or fewer** +- Focus on the essential pattern, not complete implementations +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches + +### Content Organization +- Add new patterns to existing sections when possible +- Create new sections sparingly to maintain structure +- Update the architectural principles section for major changes +- Ensure examples follow current codebase conventions + +### Quality Standards +- Test any new patterns in actual code before documenting +- Verify imports and syntax are correct for the codebase +- Keep language concise and actionable +- Focus on "what" and "how", minimize "why" explanations + +### Multiple CLAUDE.md Files +- **Directory-specific CLAUDE.md files** can be created for specialized modules +- Follow the same structure and guidelines as this root CLAUDE.md +- Keep them focused on directory-specific patterns and conventions +- Maximum 20KB per directory-specific CLAUDE.md file + +### Instructions for LLMs +When editing files in this repository, **always check for CLAUDE.md guidance**: + +1. **Look for CLAUDE.md in the same directory** as the file being edited +2. **If not found, check parent directories** recursively up to project root +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist \ No newline at end of file diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/CLAUDE.md b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/CLAUDE.md new file mode 100644 index 00000000..bbb5ad22 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/CLAUDE.md @@ -0,0 +1,350 @@ +# Emitter Module - Snowplow Java Tracker + +## Module Overview + +The emitter module handles event transmission to Snowplow collectors. It provides batching, buffering, retry logic, and asynchronous event sending with configurable HTTP clients. The BatchEmitter is the primary implementation with sophisticated error handling and exponential backoff. + +## Core Components + +``` +Emitter (interface) +└── BatchEmitter # Main implementation with batching + ├── EventStore # Event persistence interface + │ └── InMemoryEventStore # Default in-memory storage + ├── BatchPayload # POST request wrapper + ├── EmitterCallback # Success/failure callbacks + └── FailureType # Failure categorization +``` + +## Emitter Architecture Principles + +### 1. Asynchronous Processing +All events are processed asynchronously: +```java +// ✅ Events queued, not sent immediately +emitter.add(payload); // Returns quickly +// Event sent later by executor + +// ❌ Don't expect synchronous sending +emitter.add(payload); +// Event may not be sent yet! +``` + +### 2. Batch Processing Pattern +Events are batched for efficiency: +```java +// ✅ Configure batch size +EmitterConfiguration config = new EmitterConfiguration() + .batchSize(25) // Send when 25 events buffered + .bufferCapacity(1000); // Max buffer size +``` + +### 3. Retry with Exponential Backoff +Failed requests retry with increasing delays: +```java +// ✅ Automatic retry handling +// Initial: 0ms delay +// First failure: 100ms delay +// Second failure: 200ms delay +// Max delay: 600000ms (10 min) +``` + +### 4. Thread Pool Management +Configurable thread pool for sending: +```java +// ✅ Configure thread count +EmitterConfiguration config = new EmitterConfiguration() + .threadCount(2); // Number of sender threads +``` + +## BatchEmitter Implementation + +### Constructor Pattern +```java +// ✅ Use configuration objects +BatchEmitter emitter = new BatchEmitter( + networkConfig, // URL and HTTP client + emitterConfig // Batching and threading +); + +// ❌ Don't use deprecated builder +BatchEmitter.builder().url(url).build(); +``` + +### Event Addition Flow +```java +// ✅ Standard flow +boolean success = emitter.add(payload); +if (!success) { + // Buffer full, event dropped +} +``` + +### Buffer Management +```java +// ✅ Force flush buffer +emitter.flushBuffer(); + +// ✅ Close and flush +emitter.close(); // Flushes remaining events +``` + +## EventStore Pattern + +### Interface Contract +```java +public interface EventStore { + boolean add(TrackerPayload payload); + boolean remove(TrackerPayload payload); + boolean removeAll(List payloads); + List getBuffer(); + long getSize(); +} +``` + +### InMemoryEventStore Implementation +```java +// ✅ Thread-safe implementation +public class InMemoryEventStore implements EventStore { + private final AtomicLong bufferSize = new AtomicLong(0); + private final ConcurrentLinkedDeque buffer; + private final long bufferCapacity; +} +``` + +## HTTP Client Configuration + +### Client Adapter Options +```java +// ✅ OkHttp (default) +HttpClientAdapter client = new OkHttpClientAdapter(url); + +// ✅ Apache HTTP +HttpClientAdapter client = new ApacheHttpClientAdapter(url); + +// ✅ Custom implementation +HttpClientAdapter custom = new CustomAdapter(); +networkConfig.httpClientAdapter(custom); +``` + +### Cookie Management +```java +// ✅ Cookie jar for network_userid +OkHttpClientWithCookieJarAdapter adapter = + new OkHttpClientWithCookieJarAdapter(url); +``` + +## Callback Pattern + +### EmitterCallback Interface +```java +// ✅ Implement callbacks +EmitterCallback callback = new EmitterCallback() { + @Override + public void onSuccess(List payloads) { + // Handle successful send + } + + @Override + public void onFailure(FailureType type, boolean willRetry, + List payloads) { + // Handle failure + } +}; +``` + +### Failure Types +```java +public enum FailureType { + REJECTED_BY_COLLECTOR, // 4xx responses + TRACKER_ISSUE, // 5xx or network errors + EMITTER_REQUEST_FAILURE // Client-side issues +} +``` + +## Custom Retry Logic + +### Status Code Configuration +```java +// ✅ Custom retry for status codes +Map customRetry = new HashMap<>(); +customRetry.put(403, false); // Don't retry 403 +customRetry.put(500, true); // Retry 500 + +EmitterConfiguration config = new EmitterConfiguration() + .customRetryForStatusCodes(customRetry); +``` + +## Request Building + +### GET Request Pattern +```java +// ✅ Single event GET request +String url = collectorUrl + "/i?" + payload.toString(); +``` + +### POST Request Pattern +```java +// ✅ Batch POST request +BatchPayload batch = new BatchPayload(); +batch.add(payload1); +batch.add(payload2); +String json = batch.toString(); +// POST to collectorUrl + "/com.snowplowanalytics.snowplow/tp2" +``` + +## Thread Safety Patterns + +### 1. Concurrent Buffer Access +```java +// ✅ Thread-safe operations +private final ConcurrentLinkedDeque buffer; +private final AtomicLong bufferSize; +``` + +### 2. Executor Management +```java +// ✅ Proper shutdown +@Override +public void close() { + isClosing = true; + flushBuffer(); + executor.shutdown(); + executor.awaitTermination(timeout, TimeUnit.SECONDS); +} +``` + +### 3. Atomic Retry Delay +```java +// ✅ Thread-safe retry counter +private final AtomicInteger retryDelay = new AtomicInteger(0); +``` + +## Testing Emitter Behavior + +### 1. Mock EventStore +```java +// ✅ Test with mock store +EventStore mockStore = mock(EventStore.class); +when(mockStore.getBuffer()).thenReturn(payloads); +``` + +### 2. MockWebServer Testing +```java +// ✅ Test HTTP interactions +MockWebServer server = new MockWebServer(); +server.enqueue(new MockResponse().setResponseCode(200)); +BatchEmitter emitter = new BatchEmitter( + new NetworkConfiguration(server.url("/").toString()), + new EmitterConfiguration() +); +``` + +### 3. Callback Testing +```java +// ✅ Verify callbacks +AtomicBoolean success = new AtomicBoolean(false); +EmitterCallback callback = new EmitterCallback() { + @Override + public void onSuccess(List payloads) { + success.set(true); + } +}; +``` + +## Common Pitfalls + +### 1. Synchronous Expectations +```java +// ❌ Wrong: Expecting immediate send +emitter.add(payload); +assert(eventSent); // May fail + +// ✅ Correct: Wait or flush +emitter.add(payload); +emitter.flushBuffer(); +Thread.sleep(100); +``` + +### 2. Ignoring Buffer Limits +```java +// ❌ Wrong: Not checking return value +emitter.add(payload); // Might be dropped + +// ✅ Correct: Check success +if (!emitter.add(payload)) { + // Handle dropped event +} +``` + +### 3. Resource Leaks +```java +// ❌ Wrong: Not closing emitter +BatchEmitter emitter = new BatchEmitter(...); +// Use emitter... +// Never closed! + +// ✅ Correct: Always close +try (BatchEmitter emitter = new BatchEmitter(...)) { + // Use emitter +} // Auto-closed +``` + +### 4. Improper Thread Count +```java +// ❌ Wrong: Too many threads +.threadCount(100) // Excessive + +// ✅ Correct: Reasonable count +.threadCount(2) // Good default +``` + +## Performance Considerations + +### Batch Size Tuning +- Small batches (1-10): Lower latency, more requests +- Medium batches (25-50): Balanced +- Large batches (100+): Higher latency, fewer requests + +### Buffer Capacity +- Set based on expected event volume +- Consider memory constraints +- Default: 10,000 events + +### Thread Count +- 1-2 threads for most applications +- More threads for high-volume scenarios +- Consider collector capacity + +## Adding Custom Emitters + +### Template for Custom Emitter +```java +public class CustomEmitter implements Emitter { + @Override + public boolean add(TrackerPayload payload) { + // Custom logic + return true; + } + + @Override + public void flushBuffer() { + // Send all buffered events + } + + @Override + public void close() { + // Cleanup resources + } +} +``` + +## Contributing to Emitter Module + +### Guidelines +1. Maintain thread safety in all operations +2. Implement proper resource cleanup in close() +3. Honor the EmitterCallback contract +4. Test retry logic with various failure scenarios +5. Document any custom retry strategies +6. Ensure buffer limits are respected \ No newline at end of file diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/CLAUDE.md b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/CLAUDE.md new file mode 100644 index 00000000..c54d2433 --- /dev/null +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/CLAUDE.md @@ -0,0 +1,293 @@ +# Events Module - Snowplow Java Tracker + +## Module Overview + +The events module defines all trackable event types in the Snowplow Java Tracker. Each event type implements the Event interface and extends AbstractEvent, providing a consistent builder-based API for event creation and immutable event objects. + +## Event Type Hierarchy + +``` +Event (interface) +└── AbstractEvent (abstract base) + ├── PageView # Web page views + ├── Structured # Generic structured events + ├── SelfDescribing # Schema-based custom events + ├── ScreenView # Mobile/app screen views + ├── Timing # Performance timing events + ├── EcommerceTransaction # Purchase events (deprecated) + └── EcommerceTransactionItem # Line items (deprecated) +``` + +## Core Event Patterns + +### 1. Builder Pattern Mandatory +Every event MUST use the builder pattern: +```java +// ✅ Correct: Builder pattern +PageView event = PageView.builder() + .pageUrl("https://example.com") + .build(); + +// ❌ Wrong: Direct instantiation +new PageView("https://example.com"); +``` + +### 2. AbstractEvent Base Class +All events extend AbstractEvent for common fields: +```java +// ✅ Inherit common behavior +public class PageView extends AbstractEvent { + public static class Builder extends AbstractEvent.Builder { + // Event-specific fields + } +} +``` + +### 3. Self-Returning Builder Pattern +Builders must return self() for chaining: +```java +// ✅ Correct self() implementation +public static class Builder extends AbstractEvent.Builder { + @Override + protected Builder self() { + return this; + } +} +``` + +### 4. Payload Generation Pattern +Each event implements getPayload(): +```java +// ✅ Standard payload creation +@Override +public TrackerPayload getPayload() { + TrackerPayload payload = new TrackerPayload(); + payload.add(Parameter.EVENT, Constants.EVENT_PAGE_VIEW); + payload.add(Parameter.PAGE_URL, pageUrl); + return putTrueTimestamp(payload); +} +``` + +## Event-Specific Patterns + +### PageView Events +```java +// ✅ Complete PageView example +PageView pageView = PageView.builder() + .pageUrl("https://example.com") // Required + .pageTitle("Example Page") // Optional + .referrer("https://google.com") // Optional + .customContext(contexts) // From AbstractEvent + .trueTimestamp(timestamp) // From AbstractEvent + .subject(eventSubject) // From AbstractEvent + .build(); +``` + +### Structured Events +```java +// ✅ Structured event with all fields +Structured event = Structured.builder() + .category("video") // Required + .action("play") // Required + .label("tutorial") // Optional + .property("intro") // Optional + .value(1.5) // Optional + .build(); +``` + +### SelfDescribing Events +```java +// ✅ Schema-based custom event +SelfDescribing event = SelfDescribing.builder() + .eventData(new SelfDescribingJson( + "iglu:com.example/event/jsonschema/1-0-0", + eventDataMap + )) + .build(); +``` + +### EcommerceTransaction Pattern +```java +// ✅ Transaction with items +EcommerceTransaction transaction = EcommerceTransaction.builder() + .orderId("ORDER-123") + .totalValue(99.99) + .items(item1, item2) // Variadic items + .build(); +// Note: Generates multiple events when tracked +``` + +## Common Implementation Requirements + +### 1. Field Validation +```java +// ✅ Validate required fields in build() +public PageView build() { + Objects.requireNonNull(pageUrl, "pageUrl cannot be null"); + if (pageUrl.isEmpty()) { + throw new IllegalArgumentException("pageUrl cannot be empty"); + } + return new PageView(this); +} +``` + +### 2. Immutability Enforcement +```java +// ✅ Final fields, no setters +public class PageView extends AbstractEvent { + private final String pageUrl; + private final String pageTitle; + // No setters, only getters +} +``` + +### 3. Context Handling +```java +// ✅ Contexts are copied, not referenced +@Override +public List getContext() { + return new ArrayList<>(this.context); +} +``` + +### 4. Timestamp Management +```java +// ✅ True timestamp is optional +Long trueTimestamp = getTrueTimestamp(); +if (trueTimestamp != null) { + payload.add(Parameter.TRUE_TIMESTAMP, Long.toString(trueTimestamp)); +} +``` + +## Event Parameters Reference + +### Common Parameters (AbstractEvent) +- `customContext`: List of SelfDescribingJson contexts +- `trueTimestamp`: User-defined timestamp (milliseconds) +- `subject`: Event-specific Subject override + +### PageView Parameters +- `pageUrl` (required): URL of the page +- `pageTitle`: Title of the page +- `referrer`: Referring URL + +### Structured Parameters +- `category` (required): Event category +- `action` (required): Event action +- `label`: Event label +- `property`: Event property +- `value`: Numeric value + +### SelfDescribing Parameters +- `eventData` (required): SelfDescribingJson with schema and data + +## Testing Event Creation + +### 1. Test Required Fields +```java +// ✅ Test validation +@Test(expected = NullPointerException.class) +public void testMissingRequiredField() { + PageView.builder().build(); // Should throw +} +``` + +### 2. Test Optional Fields +```java +// ✅ Test with nulls +PageView event = PageView.builder() + .pageUrl("https://example.com") + .pageTitle(null) // Should work + .build(); +``` + +### 3. Test Event Payload +```java +// ✅ Verify payload contents +TrackerPayload payload = event.getPayload(); +assertEquals("pv", payload.getMap().get("e")); +assertEquals(url, payload.getMap().get("url")); +``` + +## Anti-Patterns to Avoid + +### 1. Mutable Events +```java +// ❌ Never add setters +public void setPageUrl(String url) { + this.pageUrl = url; +} +``` + +### 2. Public Constructors +```java +// ❌ Don't expose constructors +public PageView(String url) { + this.pageUrl = url; +} +``` + +### 3. Direct Field Access +```java +// ❌ Don't expose mutable fields +public List context; +``` + +### 4. Missing Validation +```java +// ❌ Don't skip validation +public Event build() { + return new PageView(this); // No checks +} +``` + +## Adding New Event Types + +### Template for New Event +```java +public class NewEvent extends AbstractEvent { + private final String requiredField; + private final String optionalField; + + public static class Builder extends AbstractEvent.Builder { + private String requiredField; + private String optionalField; + + @Override + protected Builder self() { return this; } + + public Builder requiredField(String value) { + this.requiredField = value; + return self(); + } + + public NewEvent build() { + Objects.requireNonNull(requiredField); + return new NewEvent(this); + } + } + + private NewEvent(Builder builder) { + super(builder); + this.requiredField = builder.requiredField; + this.optionalField = builder.optionalField; + } + + @Override + public TrackerPayload getPayload() { + TrackerPayload payload = new TrackerPayload(); + payload.add(Parameter.EVENT, "new"); + return putTrueTimestamp(payload); + } +} +``` + +## Contributing to Events Module + +### Guidelines +1. All new events MUST extend AbstractEvent +2. All new events MUST use the builder pattern +3. Required fields MUST be validated in build() +4. Events MUST be immutable after creation +5. Test both required and optional field scenarios +6. Document schema requirements for SelfDescribing events \ No newline at end of file diff --git a/src/test/java/CLAUDE.md b/src/test/java/CLAUDE.md new file mode 100644 index 00000000..341be053 --- /dev/null +++ b/src/test/java/CLAUDE.md @@ -0,0 +1,371 @@ +# Testing Guide - Snowplow Java Tracker + +## Testing Overview + +The test suite uses JUnit 4 with JUnit 5 Vintage Engine for backward compatibility. Tests focus on unit testing individual components with extensive use of mocking for external dependencies like HTTP servers. + +## Test Organization + +``` +src/test/java/com/snowplowanalytics/snowplow/tracker/ +├── TrackerTest.java # Core tracker functionality +├── SnowplowTest.java # Factory and registry +├── SubjectTest.java # Subject data management +├── UtilsTest.java # Utility functions +├── emitter/ # Emitter layer tests +├── events/ # Event type tests +├── payload/ # Payload serialization tests +└── http/ # HTTP client tests +``` + +## Core Testing Patterns + +### 1. Mock Emitter Pattern +Essential for testing tracker behavior without network calls: +```java +// ✅ Standard MockEmitter +public static class MockEmitter implements Emitter { + public List eventList = new ArrayList<>(); + + @Override + public boolean add(TrackerPayload payload) { + eventList.add(payload); + return true; + } +} +``` + +### 2. Test Setup Pattern +Consistent test initialization: +```java +// ✅ Standard setUp +@Before +public void setUp() { + mockEmitter = new MockEmitter(); + TrackerConfiguration config = new TrackerConfiguration("AF003", "cloudfront") + .base64Encoded(false); + tracker = new Tracker(config, mockEmitter); +} +``` + +### 3. MockWebServer Pattern +For testing HTTP interactions: +```java +// ✅ HTTP testing setup +MockWebServer server = new MockWebServer(); +server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("ok")); +String url = server.url("/").toString(); +``` + +### 4. Assertion Patterns +Verify event payloads correctly: +```java +// ✅ Payload verification +TrackerPayload payload = mockEmitter.eventList.get(0); +Map map = payload.getMap(); +assertEquals("pv", map.get("e")); // Event type +assertEquals(url, map.get("url")); // Page URL +assertNotNull(map.get("eid")); // Event ID +``` + +## Event Testing Patterns + +### 1. Builder Validation Tests +```java +// ✅ Test required fields +@Test(expected = NullPointerException.class) +public void testMissingRequiredField() { + PageView.builder().build(); // Missing pageUrl +} +``` + +### 2. Optional Field Tests +```java +// ✅ Test optional fields +@Test +public void testOptionalFields() { + PageView event = PageView.builder() + .pageUrl("https://example.com") + .pageTitle(null) // Should work + .build(); + assertNotNull(event); +} +``` + +### 3. Context Testing +```java +// ✅ Test custom contexts +@Test +public void testCustomContext() { + List contexts = singletonList( + new SelfDescribingJson("schema", + Collections.singletonMap("key", "value")) + ); + PageView event = PageView.builder() + .pageUrl("https://example.com") + .customContext(contexts) + .build(); +} +``` + +## Emitter Testing Patterns + +### 1. Batch Processing Tests +```java +// ✅ Test batching behavior +@Test +public void testBatchSize() throws InterruptedException { + BatchEmitter emitter = new BatchEmitter( + networkConfig, + new EmitterConfiguration().batchSize(2) + ); + emitter.add(payload1); + emitter.add(payload2); // Should trigger send + Thread.sleep(500); + // Verify batch sent +} +``` + +### 2. Retry Logic Tests +```java +// ✅ Test retry on failure +@Test +public void testRetryLogic() { + server.enqueue(new MockResponse().setResponseCode(500)); + server.enqueue(new MockResponse().setResponseCode(200)); + // Add event and verify retry +} +``` + +### 3. Callback Tests +```java +// ✅ Test callbacks +@Test +public void testSuccessCallback() { + AtomicBoolean called = new AtomicBoolean(false); + EmitterCallback callback = new EmitterCallback() { + @Override + public void onSuccess(List payloads) { + called.set(true); + } + }; + // Verify callback invoked +} +``` + +## Payload Testing Patterns + +### 1. Serialization Tests +```java +// ✅ Test JSON serialization +@Test +public void testJsonSerialization() { + SelfDescribingJson json = new SelfDescribingJson( + "schema", + Collections.singletonMap("key", "value") + ); + String result = json.toString(); + assertTrue(result.contains("\"schema\":\"schema\"")); +} +``` + +### 2. Base64 Encoding Tests +```java +// ✅ Test encoding +@Test +public void testBase64Encoding() { + TrackerConfiguration config = new TrackerConfiguration("ns", "app") + .base64Encoded(true); + // Verify contexts are base64 encoded +} +``` + +### 3. Size Calculation Tests +```java +// ✅ Test payload size +@Test +public void testPayloadSize() { + TrackerPayload payload = new TrackerPayload(); + payload.add("key", "value"); + assertTrue(payload.getByteSize() > 0); +} +``` + +## Subject Testing Patterns + +### 1. Subject Merging Tests +```java +// ✅ Test subject override +@Test +public void testSubjectOverride() { + Subject trackerSubject = new Subject(); + trackerSubject.setUserId("tracker-user"); + + Subject eventSubject = new Subject(); + eventSubject.setUserId("event-user"); + + // Event subject should override +} +``` + +### 2. Platform Detection Tests +```java +// ✅ Test platform setting +@Test +public void testPlatformDetection() { + Subject subject = new Subject(); + subject.setPlatform(DevicePlatform.Mobile); + assertEquals("mob", subject.getSubject().get("p")); +} +``` + +## Thread Safety Testing + +### 1. Concurrent Access Tests +```java +// ✅ Test thread safety +@Test +public void testConcurrentAccess() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + for (int i = 0; i < 10; i++) { + new Thread(() -> { + tracker.track(event); + latch.countDown(); + }).start(); + } + latch.await(); + // Verify all events tracked +} +``` + +### 2. Buffer Overflow Tests +```java +// ✅ Test buffer limits +@Test +public void testBufferOverflow() { + EmitterConfiguration config = new EmitterConfiguration() + .bufferCapacity(2); + // Add 3 events, verify one dropped +} +``` + +## Test Utilities + +### 1. Event ID Validation +```java +// ✅ Validate UUID format +private boolean isValidUUID(String id) { + try { + UUID.fromString(id); + return true; + } catch (Exception e) { + return false; + } +} +``` + +### 2. Timestamp Validation +```java +// ✅ Validate timestamp +private boolean isValidTimestamp(String ts) { + try { + long timestamp = Long.parseLong(ts); + return timestamp > 0; + } catch (Exception e) { + return false; + } +} +``` + +### 3. JSON Validation +```java +// ✅ Validate JSON structure +private boolean isValidJson(String json) { + try { + new ObjectMapper().readTree(json); + return true; + } catch (Exception e) { + return false; + } +} +``` + +## Common Test Anti-Patterns + +### 1. Real Network Calls +```java +// ❌ Don't use real endpoints +BatchEmitter emitter = new BatchEmitter( + new NetworkConfiguration("https://real-collector.com"), + config +); + +// ✅ Use MockWebServer +MockWebServer server = new MockWebServer(); +``` + +### 2. Sleep Without Reason +```java +// ❌ Arbitrary sleep +Thread.sleep(5000); // Why? + +// ✅ Sleep with purpose +Thread.sleep(100); // Allow async operation +``` + +### 3. Missing Cleanup +```java +// ❌ Resources not cleaned +MockWebServer server = new MockWebServer(); +// Never shutdown + +// ✅ Proper cleanup +@After +public void tearDown() throws IOException { + server.shutdown(); +} +``` + +### 4. Overly Complex Mocks +```java +// ❌ Complex mock setup +when(mock.method1()).thenReturn(x); +when(mock.method2()).thenReturn(y); +// 20 more lines... + +// ✅ Simple test double +class SimpleEmitter implements Emitter { + // Minimal implementation +} +``` + +## Test Execution + +### Running Tests +```bash +# Run all tests +./gradlew test + +# Run specific test class +./gradlew test --tests TrackerTest + +# Run with coverage +./gradlew test jacocoTestReport +``` + +### Test Categories +- **Unit Tests**: Individual component testing +- **Integration Tests**: Component interaction +- **Concurrency Tests**: Thread safety verification +- **Performance Tests**: Not in main suite + +## Contributing Test Guidelines + +1. **Test Naming**: Use descriptive names (testPageViewWithAllFields) +2. **One Assertion Per Test**: Keep tests focused +3. **Mock External Dependencies**: Never make real network calls +4. **Test Edge Cases**: Null values, empty strings, limits +5. **Document Complex Tests**: Add comments for non-obvious logic +6. **Clean Up Resources**: Always close/shutdown in @After \ No newline at end of file