Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions e2e-cli/README.md
Original file line number Diff line number Diff line change
@@ -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 }
```
91 changes: 91 additions & 0 deletions e2e-cli/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>analytics-parent</artifactId>
<groupId>com.segment.analytics.java</groupId>
<version>3.5.5-SNAPSHOT</version>
</parent>

<groupId>com.segment.analytics.java</groupId>
<artifactId>e2e-cli</artifactId>
<version>3.5.5-SNAPSHOT</version>
<name>Analytics Java E2E CLI</name>

<description>E2E testing CLI for Segment Analytics for Java.</description>

<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.segment.analytics.java</groupId>
<artifactId>analytics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json</artifactId>
<version>1.4.1</version>
</dependency>
</dependencies>

<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>

<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>cli.MainKt</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
159 changes: 159 additions & 0 deletions e2e-cli/src/main/kotlin/cli/Main.kt
Original file line number Diff line number Diff line change
@@ -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<Map<String, Any>>
)

data class CLIInput(
val writeKey: String,
val apiHost: String,
val sequences: List<EventSequence>,
val config: CLIConfig? = null
)

private val gson = Gson()

fun main(args: Array<String>) {
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<String, Any>) {
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<String, Any> ?: emptyMap()
@Suppress("UNCHECKED_CAST")
val properties = event["properties"] as? Map<String, Any> ?: 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)
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<module>analytics-sample</module>
<module>analytics-cli</module>
<module>analytics-spring-boot-starter</module>
<module>e2e-cli</module>
</modules>

<properties>
Expand Down
Loading