From 09caf007e781aaf9dadd1738dc013f9988fdc14e Mon Sep 17 00:00:00 2001 From: Paul Boocock Date: Fri, 10 Jul 2020 16:20:58 +0100 Subject: [PATCH 01/77] 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 02/77] 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 03/77] 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 04/77] 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 05/77] 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 06/77] 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 07/77] 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 08/77] 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 09/77] 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 10/77] 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 11/77] 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 12/77] 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 13/77] 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 14/77] 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 15/77] 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 16/77] 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 17/77] 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 18/77] 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 19/77] 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 20/77] 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 21/77] 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 22/77] 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 23/77] 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 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 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 24/77] 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 25/77] 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 26/77] 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 27/77] 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 28/77] 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 29/77] 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 30/77] 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 31/77] 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 32/77] 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 33/77] 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 34/77] 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 35/77] 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 36/77] 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 37/77] 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 38/77] 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 39/77] 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 40/77] 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 41/77] 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 42/77] 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 43/77] 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 44/77] 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 45/77] 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 46/77] 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 47/77] 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 48/77] 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 49/77] 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 50/77] 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 51/77] 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 52/77] 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 53/77] 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 54/77] 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 55/77] 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 56/77] 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 57/77] 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 58/77] 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 59/77] 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 60/77] 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 61/77] 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 62/77] 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 63/77] 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 64/77] 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 65/77] 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 66/77] 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 67/77] 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 68/77] 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 69/77] 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 70/77] 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 71/77] 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 72/77] 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 73/77] 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 74/77] 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 75/77] 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 76/77] 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 77/77] 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