Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
174 changes: 83 additions & 91 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: SonarCloud Analysis

on:
push:
branches:
- master
- development
- '*_baseline'
pull_request:
branches:
- '*'
Expand Down Expand Up @@ -38,114 +43,101 @@ jobs:
- name: Build project and run tests with coverage
run: |
# Build the project
./gradlew assembleDebug --stacktrace

# Run tests with coverage - allow test failures
./gradlew testDebugUnitTest jacocoTestReport --stacktrace
TEST_RESULT=$?
if [ $TEST_RESULT -ne 0 ]; then
echo "Some tests failed, but continuing to check for coverage data..."
# Even if tests fail, JaCoCo should generate a report with partial coverage
# from the tests that did pass
fi

- name: Prepare class files for SonarQube analysis
run: |
echo "Searching for compiled class files..."

# Create the target directory
mkdir -p build/intermediates/runtime_library_classes_dir/debug

# Find all directories containing class files with better patterns
CLASS_DIRS=$(find build -name "*.class" -type f -exec dirname {} \; | sort -u | grep -E "(javac|kotlin-classes|runtime_library)" | head -10)

if [ -z "$CLASS_DIRS" ]; then
echo "WARNING: No class files found in the build directory!"
echo "Searching in all build subdirectories..."
find build -name "*.class" -type f | head -20
./gradlew assembleDebug

# Run tests - continue even if some tests fail
./gradlew testDebugUnitTest || echo "Some tests failed, but continuing to generate coverage report..."

# Generate JaCoCo aggregate report separately (ensures it runs even if tests failed)
./gradlew jacocoRootReport

# Log report location for debugging
REPORT_PATH="build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
if [ -f "$REPORT_PATH" ]; then
echo "✓ JaCoCo report generated at: $REPORT_PATH ($(wc -c < "$REPORT_PATH") bytes)"
else
echo "Found class files in the following directories:"
echo "$CLASS_DIRS"

# Copy classes from all relevant directories, not just the first one
for CLASS_DIR in $CLASS_DIRS; do
if [ -d "$CLASS_DIR" ] && [ "$(find "$CLASS_DIR" -name "*.class" | wc -l)" -gt 0 ]; then
echo "Copying classes from $CLASS_DIR"
cp -r "$CLASS_DIR"/* build/intermediates/runtime_library_classes_dir/debug/ 2>/dev/null || echo "Failed to copy from $CLASS_DIR"
fi
done

# Verify the target directory now has class files
CLASS_COUNT=$(find build/intermediates/runtime_library_classes_dir/debug -name "*.class" | wc -l)
echo "Target directory now contains $CLASS_COUNT class files"
echo "✗ JaCoCo report was NOT generated at: $REPORT_PATH"
fi

# Update sonar-project.properties with all found class directories
echo "" >> sonar-project.properties
echo "# Additional binary paths found during build" >> sonar-project.properties
if [ -n "$CLASS_DIRS" ]; then
# Convert newlines to commas for sonar.java.binaries
BINARY_PATHS=$(echo "$CLASS_DIRS" | tr '\n' ',' | sed 's/,$//')
echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug,$BINARY_PATHS" >> sonar-project.properties
- name: Verify class files and coverage data for SonarQube analysis
run: |
echo "=== Verifying Build Artifacts for SonarQube ==="
echo ""

# Dynamically get modules from settings.gradle (extract module names from "include ':modulename'" lines)
MODULES=$(grep "^include" settings.gradle | cut -d"'" -f2 | cut -d":" -f2 | tr '\n' ' ')
echo "Detected modules: $MODULES"
echo ""

echo "Checking compiled class files for each module:"
for module in $MODULES; do
MODULE_CLASSES_DIR="${module}/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes"
if [ -d "$MODULE_CLASSES_DIR" ]; then
CLASS_COUNT=$(find "$MODULE_CLASSES_DIR" -name "*.class" | wc -l)
echo " ✓ ${module}: Found $CLASS_COUNT class files in $MODULE_CLASSES_DIR"
else
echo " ✗ ${module}: Class directory not found: $MODULE_CLASSES_DIR"
fi
done

echo ""
echo "Checking JaCoCo coverage report:"
if [ -f build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml ]; then
REPORT_SIZE=$(wc -c < build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml)
PACKAGE_COUNT=$(grep -c "<package" build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml || echo "0")
CLASS_COUNT=$(grep -c "<class" build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml || echo "0")
LINE_COUNT=$(grep -c "<line" build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml || echo "0")
COVERED_LINES=$(grep -c 'ci="1"' build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml || echo "0")

echo " ✓ JaCoCo report found: build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
echo " - Size: $REPORT_SIZE bytes"
echo " - Packages: $PACKAGE_COUNT"
echo " - Classes: $CLASS_COUNT"
echo " - Lines: $LINE_COUNT"
echo " - Covered lines: $COVERED_LINES"

# Check if events module coverage is present
if grep -q 'package name="io/harness/events"' build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml; then
echo " ✓ Events module (io/harness/events) coverage found in report"
else
echo " ✗ WARNING: Events module coverage NOT found in report"
fi
else
echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug" >> sonar-project.properties
echo " ✗ JaCoCo report file not found"
fi

echo "Checking for JaCoCo report files..."
find build -name "*.xml" | grep jacoco || echo "No JaCoCo XML files found"
find build -name "*.exec" | grep jacoco || echo "No JaCoCo exec files found"

echo "Contents of JaCoCo report directory:"
ls -la build/reports/jacoco/jacocoTestReport/ || echo "Directory not found"


echo ""
echo "Checking JaCoCo execution data for each module:"
for module in $MODULES; do
EXEC_FILE="${module}/build/jacoco/testDebugUnitTest.exec"
if [ -f "$EXEC_FILE" ]; then
EXEC_SIZE=$(wc -c < "$EXEC_FILE")
echo " ✓ ${module}: Found execution data ($EXEC_SIZE bytes) in $EXEC_FILE"
else
echo " ✗ ${module}: Execution data not found: $EXEC_FILE"
fi
done

echo ""
echo "Checking test execution results:"
echo "Test execution summary:"
TEST_RESULT_FILES=$(find build -name "TEST-*.xml" 2>/dev/null)
if [ -n "$TEST_RESULT_FILES" ]; then
echo "Found test result files:"
echo "$TEST_RESULT_FILES"
# Count total tests, failures, errors
TOTAL_TESTS=$(cat $TEST_RESULT_FILES | grep -o 'tests="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}')
TOTAL_FAILURES=$(cat $TEST_RESULT_FILES | grep -o 'failures="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}')
TOTAL_ERRORS=$(cat $TEST_RESULT_FILES | grep -o 'errors="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}')
echo "Test summary: $TOTAL_TESTS tests, $TOTAL_FAILURES failures, $TOTAL_ERRORS errors"
else
echo "No test result files found"
fi

echo ""
echo "Checking JaCoCo report content:"
if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then
echo "Report file size: $(wc -c < build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) bytes"
echo "First 500 chars of report:"
head -c 500 build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml
echo ""
echo ""
echo "Counting coverage elements:"
grep -c "<package" build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml || echo "No packages found"
grep -c "<class" build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml || echo "No classes found"
grep -c "<method" build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml || echo "No methods found"
grep -c "<line" build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml || echo "No lines found"
grep -c 'ci="1"' build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml || echo "No covered lines found"
echo " Tests: $TOTAL_TESTS | Failures: $TOTAL_FAILURES | Errors: $TOTAL_ERRORS"
else
echo "JaCoCo report file not found"
echo " No test result files found"
fi

echo ""
echo "Checking binary directories specified in sonar-project.properties:"
echo "build/intermediates/runtime_library_classes_dir/debug:"
ls -la build/intermediates/runtime_library_classes_dir/debug || echo "Directory not found"


echo ""
echo "Checking all available class directories:"
find build -path "*/build/*" -name "*.class" | head -n 10 || echo "No class files found"

echo ""
echo "Final sonar-project.properties content:"
echo "SonarQube configuration (sonar-project.properties):"
cat sonar-project.properties
echo ""
echo "=== Verification Complete ==="

- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@v6
uses: SonarSource/sonarqube-scan-action@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2025 Split Software, Inc.
Copyright © 2026 Split Software, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
1 change: 1 addition & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
5 changes: 5 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# API module

This module contains the public API interfaces and types exposed to consumers of the Split SDK.

Classes in this module are part of the public API contract and should maintain backwards compatibility.
22 changes: 22 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id 'com.android.library'
}

apply from: "$rootDir/gradle/common-android-library.gradle"

android {
namespace 'io.split.android.client.api'

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation libs.annotation

testImplementation libs.junit4
testImplementation libs.mockitoCore
}

Empty file added api/consumer-rules.pro
Empty file.
22 changes: 22 additions & 0 deletions api/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Empty file.
5 changes: 5 additions & 0 deletions api/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Map;

import io.split.android.client.attributes.AttributesManager;
import io.split.android.client.events.SplitEventListener;
import io.split.android.client.events.SplitEvent;
import io.split.android.client.events.SplitEventTask;

Expand Down Expand Up @@ -179,6 +180,42 @@ public interface SplitClient extends AttributesManager {

void on(SplitEvent event, SplitEventTask task);

/**
* Registers an event listener for SDK events that provide typed metadata.
* <p>
* This method provides type-safe callbacks for SDK_READY, SDK_UPDATE, and SDK_READY_FROM_CACHE events.
* Override the methods you need in the listener.
* <p>
* Multiple listeners can be registered. Each listener will be invoked once per event.
* <p>
* Example usage:
* <pre>{@code
* client.addEventListener(new SdkEventListener() {
* @Override
* public void onReady(SplitClient client, SdkReadyMetadata metadata) {
* Boolean initialCacheLoad = metadata.isInitialCacheLoad();
* // Handle SDK ready on background thread
* }
*
* @Override
* public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
* SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE
* List<String> names = metadata.getNames(); // updated flag/segment names
* // Handle on background thread
* }
*
* @Override
* public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) {
* // Handle on main/UI thread
* Boolean initialCacheLoad = metadata.isInitialCacheLoad();
* }
* });
* }</pre>
*
* @param listener the event listener to register. Must not be null.
*/
void addEventListener(@NonNull SplitEventListener listener);

/**
* Enqueue a new event to be sent to Split data collection services.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.split.android.client.events;

import androidx.annotation.Nullable;

/**
* Typed metadata for SDK_READY and SDK_READY_FROM_CACHE events.
* <p>
* Contains information about the cache state when the SDK becomes ready.
*/
public final class SdkReadyMetadata {

@Nullable
private final Boolean mInitialCacheLoad;

@Nullable
private final Long mLastUpdateTimestamp;

/**
* Creates a new SdkReadyMetadata instance.
*
* @param initialCacheLoad true if this is an initial cache load with no usable cache, or null if not available
* @param lastUpdateTimestamp the last successful cache timestamp in milliseconds since epoch, or null if not available
*/
public SdkReadyMetadata(@Nullable Boolean initialCacheLoad, @Nullable Long lastUpdateTimestamp) {
mInitialCacheLoad = initialCacheLoad;
mLastUpdateTimestamp = lastUpdateTimestamp;
}

/**
* Returns whether this is an initial cache load with no usable cache.
* <p>
* This is true when the SDK starts without any prior cached data (fresh install),
* meaning data was fetched from the server for the first time.
*
* @return true if initial cache load, false otherwise, or null if not available
*/
@Nullable
public Boolean isInitialCacheLoad() {
return mInitialCacheLoad;
}

/**
* Returns the last successful cache timestamp in milliseconds since epoch.
*
* @return the timestamp, or null if not available
*/
@Nullable
public Long getLastUpdateTimestamp() {
return mLastUpdateTimestamp;
}
}

Loading
Loading