diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..6b567ad4 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,78 @@ +# 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 }} + E2E_TEST_SUITES: basic,retry + # E2E_TEST_SKIP: exponential-backoff # skip specific test files if needed + 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 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 } +``` 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