diff --git a/CHANGELOG b/CHANGELOG index ca3cdbdc..c750412a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,26 @@ +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) +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!) + Java 1.0.0 (2022-09-06) ----------------------- Add close() to Emitter interface and Tracker (#357) 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/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:// 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 CookieJar cookieJar = null; // 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(); - } - - /** - * 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. - * - * @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) - .cookieJar(cookieJar); - - EmitterConfiguration emitterConfig = new EmitterConfiguration() - .batchSize(batchSize) - .bufferCapacity(bufferCapacity) - .eventStore(eventStore) - .customRetryForStatusCodes(customRetryForStatusCodes) - .threadCount(threadCount) - .requestExecutorService(requestExecutorService) - .callback(callback); - - return new BatchEmitter(networkConfig, emitterConfig); - } - } - - 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. * @@ -240,8 +67,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 +83,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/emitter/BatchPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java index cf39dd2d..7cc0187c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchPayload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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/emitter/Emitter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java index 58a74240..56f71fdc 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/Emitter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java index a79ecc68..6535b7f2 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EmitterCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java index f78a074f..f165bff8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/EventStore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java index b3c2cd95..ca9eb792 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/FailureType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java index b53a711d..dc29c49d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java index 9a9dc141..5731a6d5 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/AbstractEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java index 22d82b21..f3df73d0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java index 0f72ca3f..ca334866 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/EcommerceTransactionItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java index 0c0ed380..8b66023d 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Event.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java index 536a6685..52482ef8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/PageView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java index b2445f73..648cc702 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/ScreenView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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 b2a4b90f..b515b7e6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/SelfDescribing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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/events/Structured.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java index 953a94a0..299eb740 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Structured.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java index 729f9074..e152ff9a 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/events/Timing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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 1346a44d..58f586f6 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/AbstractHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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 f2089cb0..de72604c 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/ApacheHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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; @@ -44,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. @@ -117,9 +63,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 +86,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/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java index d300ccbb..6439c6b0 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookie.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java index ac1a683a..5f049039 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/CollectorCookieJar.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java index e971d6a7..105d02b7 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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..95df941b 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/http/OkHttpClientAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -45,57 +45,8 @@ public OkHttpClientAdapter(String url, OkHttpClient httpClient) { this.httpClient = httpClient; } - /** - * @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; + public OkHttpClientAdapter(String url) { + this(url, new OkHttpClient.Builder().build()); } /** @@ -123,9 +74,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 +104,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/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/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java index 4014fc05..31389e46 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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 542b71c7..915c17d8 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJson.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java index 260b27a3..eddbfa2f 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerParameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java index d3328e11..089f4346 100644 --- a/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java +++ b/src/main/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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 diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java index 2734eb28..b245f590 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SnowplowTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java index 38bad0fe..7af8f4b3 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/SubjectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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() diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java index 208a5afc..c2bbbd36 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/TrackerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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; @@ -577,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.0", tracker.getTrackerVersion()); + assertEquals("java-2.1.0", tracker.getTrackerVersion()); } @Test diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java index 214ba30e..24ba8e44 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/UtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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 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 a1159ac4..dc9efb63 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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); } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java index e2d35971..0eebdc6f 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/BatchEmitterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java index 6959b280..b50a9047 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/CollectorCookieJarTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java index dc499b78..e9552746 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/emitter/InMemoryEventStoreTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. 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/http/HttpClientAdapterTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java index 710aa6c9..031e67ce 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/http/HttpClientAdapterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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; @@ -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()); @@ -126,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")); 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 2c763e94..68f144e1 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/SelfDescribingJsonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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. @@ -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); + } } diff --git a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java index 6b2db9eb..0034a648 100644 --- a/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java +++ b/src/test/java/com/snowplowanalytics/snowplow/tracker/payload/TrackerPayloadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2014-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.