From 8b629a48ec1d6d49413e943c3441b43a141599b7 Mon Sep 17 00:00:00 2001 From: eusorov Date: Thu, 26 Oct 2023 12:42:20 +0200 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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