From c75e594b2d65387556a8ad7230aa762e1845c47a Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 4 Feb 2026 14:28:24 -0500 Subject: [PATCH 1/4] Add e2e-cli module for analytics-java - Kotlin CLI using Gson for JSON parsing - Separate from existing analytics-cli to avoid disruption - Added to parent pom.xml modules Co-Authored-By: Claude Opus 4.5 --- e2e-cli/pom.xml | 91 ++++++++++++++++ e2e-cli/src/main/kotlin/cli/Main.kt | 159 ++++++++++++++++++++++++++++ pom.xml | 1 + 3 files changed, 251 insertions(+) create mode 100644 e2e-cli/pom.xml create mode 100644 e2e-cli/src/main/kotlin/cli/Main.kt diff --git a/e2e-cli/pom.xml b/e2e-cli/pom.xml new file mode 100644 index 00000000..b5782b8f --- /dev/null +++ b/e2e-cli/pom.xml @@ -0,0 +1,91 @@ + + + + 4.0.0 + + + analytics-parent + com.segment.analytics.java + 3.5.5-SNAPSHOT + + + com.segment.analytics.java + e2e-cli + 3.5.5-SNAPSHOT + Analytics Java E2E CLI + + E2E testing CLI for Segment Analytics for Java. + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + com.segment.analytics.java + analytics + ${project.version} + + + org.jetbrains.kotlinx + kotlinx-serialization-json + 1.4.1 + + + + + src/main/kotlin + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + compile + compile + + compile + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + jar-with-dependencies + + + + cli.MainKt + + + + + + package + + single + + + + + + + diff --git a/e2e-cli/src/main/kotlin/cli/Main.kt b/e2e-cli/src/main/kotlin/cli/Main.kt new file mode 100644 index 00000000..6db10cbd --- /dev/null +++ b/e2e-cli/src/main/kotlin/cli/Main.kt @@ -0,0 +1,159 @@ +package cli + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.segment.analytics.Analytics +import com.segment.analytics.Callback +import com.segment.analytics.messages.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +data class CLIOutput( + val success: Boolean, + val error: String? = null, + val sentBatches: Int = 0 +) + +data class CLIConfig( + val flushAt: Int? = null, + val flushInterval: Long? = null, + val maxRetries: Int? = null, + val timeout: Int? = null +) + +data class EventSequence( + val delayMs: Long = 0, + val events: List> +) + +data class CLIInput( + val writeKey: String, + val apiHost: String, + val sequences: List, + val config: CLIConfig? = null +) + +private val gson = Gson() + +fun main(args: Array) { + var output = CLIOutput(success = false, error = "Unknown error") + + try { + // Parse --input argument + val inputIndex = args.indexOf("--input") + if (inputIndex == -1 || inputIndex + 1 >= args.size) { + throw IllegalArgumentException("Missing required --input argument") + } + + val inputJson = args[inputIndex + 1] + val input = gson.fromJson(inputJson, CLIInput::class.java) + + val flushAt = input.config?.flushAt ?: 20 + val flushIntervalMs = input.config?.flushInterval ?: 10000L + + val flushLatch = CountDownLatch(1) + val hasError = AtomicBoolean(false) + var errorMessage: String? = null + + val analytics = Analytics.builder(input.writeKey) + .endpoint(input.apiHost) + .flushQueueSize(flushAt) + .flushInterval(maxOf(flushIntervalMs, 1000L), TimeUnit.MILLISECONDS) + .callback(object : Callback { + override fun success(message: Message?) { + // Event sent successfully + } + + override fun failure(message: Message?, throwable: Throwable?) { + hasError.set(true) + errorMessage = throwable?.message + } + }) + .build() + + // Process event sequences + for (seq in input.sequences) { + if (seq.delayMs > 0) { + Thread.sleep(seq.delayMs) + } + + for (event in seq.events) { + sendEvent(analytics, event) + } + } + + // Flush and shutdown + analytics.flush() + analytics.shutdown() + + output = if (hasError.get()) { + CLIOutput(success = false, error = errorMessage, sentBatches = 0) + } else { + CLIOutput(success = true, sentBatches = 1) + } + + } catch (e: Exception) { + output = CLIOutput(success = false, error = e.message ?: e.toString()) + } + + println(gson.toJson(output)) +} + +fun sendEvent(analytics: Analytics, event: Map) { + val type = event["type"] as? String + ?: throw IllegalArgumentException("Event missing 'type' field") + + val userId = event["userId"] as? String ?: "" + val anonymousId = event["anonymousId"] as? String + val messageId = event["messageId"] as? String + @Suppress("UNCHECKED_CAST") + val traits = event["traits"] as? Map ?: emptyMap() + @Suppress("UNCHECKED_CAST") + val properties = event["properties"] as? Map ?: emptyMap() + val eventName = event["event"] as? String + val name = event["name"] as? String + val groupId = event["groupId"] as? String + val previousId = event["previousId"] as? String + + val messageBuilder: MessageBuilder<*, *> = when (type) { + "identify" -> { + IdentifyMessage.builder().apply { + traits(traits) + } + } + "track" -> { + TrackMessage.builder(eventName ?: "Unknown Event").apply { + properties(properties) + } + } + "page" -> { + PageMessage.builder(name ?: "Unknown Page").apply { + properties(properties) + } + } + "screen" -> { + ScreenMessage.builder(name ?: "Unknown Screen").apply { + properties(properties) + } + } + "alias" -> { + AliasMessage.builder(previousId ?: "") + } + "group" -> { + GroupMessage.builder(groupId ?: "").apply { + traits(traits) + } + } + else -> throw IllegalArgumentException("Unknown event type: $type") + } + + if (userId.isNotEmpty()) { + messageBuilder.userId(userId) + } + if (anonymousId != null) { + messageBuilder.anonymousId(anonymousId) + } + + analytics.enqueue(messageBuilder) +} diff --git a/pom.xml b/pom.xml index 70cfe8b4..d873d8ba 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ analytics-sample analytics-cli analytics-spring-boot-starter + e2e-cli From 68bb74afaf7b15e0afbde2b53e4778cb6006421f Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 4 Feb 2026 18:15:51 -0500 Subject: [PATCH 2/4] Add E2E test workflow Runs sdk-e2e-tests suite against the e2e-cli to verify SDK behavior. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/e2e-tests.yml | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/e2e-tests.yml diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..61ddc162 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,76 @@ +# E2E Tests for analytics-java +# Copy this file to: analytics-java/.github/workflows/e2e-tests.yml +# +# This workflow: +# 1. Checks out the SDK and sdk-e2e-tests repos +# 2. Builds the SDK and e2e-cli +# 3. Runs the e2e test suite + +name: E2E Tests + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: # Allow manual trigger + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout SDK + uses: actions/checkout@v4 + with: + path: sdk + + - name: Checkout sdk-e2e-tests + uses: actions/checkout@v4 + with: + repository: segmentio/sdk-e2e-tests + path: sdk-e2e-tests + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build Java SDK and e2e-cli + working-directory: sdk + run: mvn package -pl e2e-cli -am -DskipTests + + - name: Find e2e-cli jar + id: find-jar + working-directory: sdk + run: | + JAR_PATH=$(find e2e-cli/target -name "e2e-cli-*-jar-with-dependencies.jar" | head -1) + echo "jar_path=$JAR_PATH" >> $GITHUB_OUTPUT + + - name: Install sdk-e2e-tests dependencies + working-directory: sdk-e2e-tests + run: npm ci + + - name: Build sdk-e2e-tests + working-directory: sdk-e2e-tests + run: npm run build + + - name: Run E2E tests + working-directory: sdk-e2e-tests + env: + CLI_COMMAND: java -jar ${{ github.workspace }}/sdk/${{ steps.find-jar.outputs.jar_path }} + run: npm test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results + path: sdk-e2e-tests/test-results/ + if-no-files-found: ignore From 9235f2e3cf40cd676ce9e58d3b9a6e2157736816 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 9 Feb 2026 11:59:28 -0500 Subject: [PATCH 3/4] Add E2E_TEST_SUITES to e2e workflow for selective test execution Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 61ddc162..6b567ad4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -65,6 +65,8 @@ jobs: working-directory: sdk-e2e-tests env: CLI_COMMAND: java -jar ${{ github.workspace }}/sdk/${{ steps.find-jar.outputs.jar_path }} + E2E_TEST_SUITES: basic,retry + # E2E_TEST_SKIP: exponential-backoff # skip specific test files if needed run: npm test - name: Upload test results From 90efa6063e9278c91ce7d4d0878fec806a4f1a4f Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 9 Feb 2026 17:20:50 -0500 Subject: [PATCH 4/4] Adding README.md for e2e-cli --- e2e-cli/README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 e2e-cli/README.md diff --git a/e2e-cli/README.md b/e2e-cli/README.md new file mode 100644 index 00000000..b319749e --- /dev/null +++ b/e2e-cli/README.md @@ -0,0 +1,54 @@ +# analytics-java e2e-cli + +E2E test CLI for the [analytics-java](https://github.com/segmentio/analytics-java) SDK. Accepts a JSON input describing events and SDK configuration, sends them through the real SDK, and outputs results as JSON. + +Built with Kotlin (JVM) and packaged as a fat jar via Maven. + +## Setup + +```bash +mvn package -pl e2e-cli -am +``` + +## Usage + +```bash +java -jar e2e-cli/target/e2e-cli-*-jar-with-dependencies.jar --input '{"writeKey":"...", ...}' +``` + +## Input Format + +```jsonc +{ + "writeKey": "your-write-key", // required + "apiHost": "https://...", // optional — SDK default if omitted + "sequences": [ // required — event sequences to send + { + "delayMs": 0, + "events": [ + { "type": "track", "event": "Test", "userId": "user-1" } + ] + } + ], + "config": { // optional + "flushAt": 250, + "flushInterval": 10000, + "maxRetries": 3, + "timeout": 15 + } +} +``` + +Note: Java is a server-side SDK — there is no CDN settings fetch, so `cdnHost` does not apply. + +## Output Format + +```json +{ "success": true, "sentBatches": 1 } +``` + +On failure: + +```json +{ "success": false, "error": "description", "sentBatches": 0 } +```