Skip to content
Merged
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
29 changes: 29 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Release

on:
push:
tags: ["v*"]

permissions:
contents: read
packages: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: gradle
- uses: gradle/actions/setup-gradle@v4
- name: Verify tag matches SDK version
run: test "${GITHUB_REF_NAME#v}" = "$(sed -n 's/version = "\(.*\)"/\1/p' flextrack/build.gradle.kts | head -1)"
- name: Verify release
run: ./gradlew clean :flextrack:test :flextrack:lint :flextrack:assembleRelease
- name: Publish to GitHub Packages
run: ./gradlew :flextrack:publishReleasePublicationToGitHubPackagesRepository
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Changelog

## Unreleased
## 1.0.0 - 2026-08-17

### Added

- Android library and sample module foundation.
- Maven publication metadata and local publishing support.
- FlexTrack Core Specification 1.0.0 contract baseline.
- CI quality gates for build, unit tests, lint, and publication verification.
- Immutable events, enrichment, consent-aware routing, and deterministic sampling.
- Coroutine-based tracker runtime with isolated delivery failures.
- In-memory and durable atomic-file offline queues with selective retry.
- Shared Flutter/Kotlin conformance test runner.
- Runnable Android sample and GitHub Packages release workflow.
93 changes: 85 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,84 @@ The Kotlin SDK targets Android API 21+ and implements
[FlexTrack Core Specification 1.0.0](contract/README.md), shared with
[FlexTrack Flutter 2.1.0](https://pub.dev/packages/flex_track).

## Project status
## Features

The SDK is under active development toward Kotlin 1.0.0. The current version is
`0.1.0-SNAPSHOT` and is not ready for production use.
- Immutable events with stable IDs and UTC timestamps.
- Priority routing, tracker groups, consent gates, and deterministic sampling.
- Ordered enrichment with transformer failure isolation.
- Coroutine-native tracker lifecycle and concurrent delivery.
- Per-tracker failure isolation and selective retry.
- Durable, atomic offline queue for Android plus an in-memory test queue.
- Shared conformance fixtures verified against the Flutter implementation.

## Installation

The release workflow publishes version `1.0.0` through GitHub Packages. After
the release tag is pushed, add the repository and credentials to your Gradle
settings, then add the dependency:

```kotlin
repositories {
maven {
url = uri("https://maven.pkg.github.com/alirezat66/flex_track_kotlin")
credentials {
username = providers.gradleProperty("gpr.user").orNull
password = providers.gradleProperty("gpr.key").orNull
}
}
}
```

```kotlin
dependencies {
implementation("dev.taghizadeh.flextrack:flextrack:1.0.0")
}
```

GitHub Packages requires a GitHub username and a token with `read:packages`.

## Quick start

Implement an event and a destination adapter:

```kotlin
class PurchaseEvent : FlexEvent() {
override val name = "purchase"
override val properties = mapOf("plan" to "pro")
}

class AnalyticsTracker : Tracker {
override val id = "analytics"
override suspend fun track(event: FlexEvent) {
// Forward the event to your analytics vendor.
}
}
```

Configure routing and use the lifecycle-aware client:

```kotlin
val group = TrackerGroup("product", listOf("analytics"))
val client = FlexTrackClient(
routingEngine = RoutingEngine(
RoutingConfiguration(
rules = listOf(RoutingRule(targetGroup = group)),
),
),
queue = FileEventQueue(applicationContext),
consentProvider = { ConsentState(general = true) },
)

client.register(AnalyticsTracker())
client.start()
val result = client.track(PurchaseEvent())
client.flush()
client.shutdown()
```

All client operations are `suspend` functions. Call them from an application-
owned coroutine scope. `FileEventQueue` stores failed/offline deliveries in the
app's private files directory and retries only the destinations still pending.

## Modules

Expand All @@ -25,12 +99,15 @@ The SDK is under active development toward Kotlin 1.0.0. The current version is
./gradlew :flextrack:publishToMavenLocal
```

## Roadmap
## Release

The tag must match the version in `flextrack/build.gradle.kts`. Pushing a tag
such as `v1.0.0` verifies the library and publishes it to GitHub Packages:

1. Library foundation and shared contract.
2. Event, enrichment, routing, consent, and deterministic sampling.
3. Tracker runtime, client lifecycle, debug records, and conformance.
4. Sample application, documentation, and Kotlin 1.0.0 release.
```bash
git tag v1.0.0
git push origin v1.0.0
```

## License

Expand Down
16 changes: 15 additions & 1 deletion flextrack/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = "dev.taghizadeh.flextrack"
version = "0.1.0-SNAPSHOT"
version = "1.0.0"

android {
namespace = "dev.taghizadeh.flextrack"
Expand Down Expand Up @@ -56,6 +56,20 @@ dependencies {
}

publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/alirezat66/flex_track_kotlin")
credentials {
username = providers.gradleProperty("gpr.user")
.orElse(providers.environmentVariable("GITHUB_ACTOR"))
.orNull
password = providers.gradleProperty("gpr.key")
.orElse(providers.environmentVariable("GITHUB_TOKEN"))
.orNull
}
}
}
publications {
register<MavenPublication>("release") {
groupId = project.group.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package dev.taghizadeh.flextrack

/** Package metadata for the FlexTrack Kotlin SDK. */
public object FlexTrack {
/** Current pre-release SDK version. */
public const val VERSION: String = "0.1.0-SNAPSHOT"
/** Current SDK version. */
public const val VERSION: String = "1.0.0"

/** Language-neutral FlexTrack Core specification implemented by this SDK. */
public const val CORE_SPEC_VERSION: String = "1.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test
class FlexTrackTest {
@Test
fun `package metadata identifies SDK and contract versions`() {
assertEquals("0.1.0-SNAPSHOT", FlexTrack.VERSION)
assertEquals("1.0.0", FlexTrack.VERSION)
assertEquals("1.0.0", FlexTrack.CORE_SPEC_VERSION)
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ material = { group = "com.google.android.material", name = "material", version.r
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
json = { module = "org.json:json", version.ref = "json" }

[plugins]
Expand Down
3 changes: 3 additions & 0 deletions sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ android {
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
Expand All @@ -38,10 +39,12 @@ android {
}

dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(project(":flextrack"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
13 changes: 11 additions & 2 deletions sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FlexTrackKotlin" />
android:theme="@style/Theme.FlexTrackKotlin">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package dev.taghizadeh.flextrack.sample

import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import dev.taghizadeh.flextrack.event.FlexEvent
import dev.taghizadeh.flextrack.routing.ConsentState
import dev.taghizadeh.flextrack.routing.RoutingConfiguration
import dev.taghizadeh.flextrack.routing.RoutingEngine
import dev.taghizadeh.flextrack.routing.RoutingRule
import dev.taghizadeh.flextrack.routing.TrackerGroup
import dev.taghizadeh.flextrack.runtime.FileEventQueue
import dev.taghizadeh.flextrack.runtime.FlexTrackClient
import dev.taghizadeh.flextrack.runtime.Tracker
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
private val scope = MainScope()
private lateinit var output: TextView
private lateinit var client: FlexTrackClient

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
output = TextView(this).apply { text = getString(R.string.ready) }
val track = Button(this).apply {
text = getString(R.string.track_purchase)
setOnClickListener { sendPurchase() }
}
setContentView(LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
val padding = (24 * resources.displayMetrics.density).toInt()
setPadding(padding, padding, padding, padding)
addView(track)
addView(output)
})

client = FlexTrackClient(
routingEngine = RoutingEngine(
RoutingConfiguration(
rules = listOf(
RoutingRule(
targetGroup = TrackerGroup("analytics", listOf("console")),
),
),
),
),
queue = FileEventQueue(this),
consentProvider = { ConsentState(general = true) },
)
scope.launch {
client.register(ConsoleTracker { output.text = it })
client.start()
client.flush()
}
}

private fun sendPurchase() {
scope.launch {
val result = client.track(PurchaseEvent())
output.text = getString(
R.string.result,
result.successfulTrackerIds.joinToString(),
result.queuedTrackerIds.joinToString(),
)
}
}

override fun onDestroy() {
scope.launch { client.shutdown() }.invokeOnCompletion { scope.cancel() }
super.onDestroy()
}
}

private class PurchaseEvent : FlexEvent() {
override val name: String = "purchase"
override val properties: Map<String, Any?> = mapOf("plan" to "pro", "currency" to "EUR")
}

private class ConsoleTracker(private val log: (String) -> Unit) : Tracker {
override val id: String = "console"

override suspend fun track(event: FlexEvent) {
log("${event.name}: ${event.properties}")
}
}
5 changes: 4 additions & 1 deletion sample/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<resources>
<string name="app_name">FlexTrack Kotlin</string>
</resources>
<string name="ready">FlexTrack is ready.</string>
<string name="track_purchase">Track purchase</string>
<string name="result">Delivered: %1$s\nQueued: %2$s</string>
</resources>
Loading