This document captures the development conventions, architecture, and rules for the Embrace Android SDK. It is intended for AI coding agents and human contributors alike.
Canonical sources: This file distills conventions from
CONTRIBUTING.md,buildSrc/,config/detekt/,.editorconfig, module READMEs, and build scripts. When those sources change, this file should be updated.Last update: February 9 2026. AI agents should regenerate this document when the project's structure or tooling materially changes or seems out-of-date, and allow humans to review the changes.
The Embrace Android SDK is an observability SDK for Android apps built on OpenTelemetry.
It captures performance telemetry (spans, logs, crashes, network requests) and delivers it to the Embrace backend.
It is published to Maven Central under the io.embrace group.
| Tool | Version / Notes |
|---|---|
| Language | Kotlin (primary), some minimal Java/C++ |
| Min Supported Kotlin version | 2.0 |
| Kotlin compile-time version | 2.3 |
| JVM target | 11 |
| Android minSdk | 21 |
| Android compileSdk | 36 |
| Build system | Gradle (Kotlin DSL) with convention plugins in buildSrc/ |
| Dependency catalog | gradle/libs.versions.toml |
| Java | 21 (CI), 11 (target compatibility) |
| Serialization | Moshi (with KSP codegen) |
| OTel | io.opentelemetry.kotlin (Kotlin-friendly OpenTelemetry wrappers) |
| HTTP | OkHttp 4.x |
| Configuration cache | Enabled, problems=fail |
# Full build (compile + lint + detekt + unit tests)
./gradlew build
# Build excluding slow integration tests
./gradlew build -x embrace-gradle-plugin-integration-tests:test
# Run tests for a specific module
./gradlew :embrace-android-core:test
# Run integration tests (in the SDK module)
./gradlew :embrace-android-sdk:test
# Code coverage report (XML)
./gradlew koverXmlReport
# Build the example app
cd examples/ExampleApp && ./gradlew bundleRelease
# Update binary compatibility API dumps
./gradlew apiDumpThe project has several modules organized into layers:
These use the embrace-public-api-conventions plugin, which enforces kotlin.explicitApi(),
binary compatibility validation, and Dokka documentation.
embrace-android-api- Public API surface for SDK consumers (spans, logs, network, user, session APIs)embrace-android-sdk- Main SDK entrypoint; aggregates all instrumentation and wires everything togetherembrace-android-otel-java- Java-compatible OTel bindings
embrace-android-core- Main implementation module; hidden from library consumers viaembrace-android-sdkembrace-android-infra- JVM-only core infrastructure types used across most modules (e.g.,InternalLogger,BackgroundWorker,Clock)embrace-android-utils- Android Framework utilitiesembrace-internal-api- APIs shared across Embrace modules and Embrace's React Native/Unity/Flutter SDKs, but not exposed to consumers
embrace-android-payload- Data models for HTTP payloads (Moshi@JsonClassdata classes)embrace-android-envelope- Envelope wrapping for payloadsembrace-android-delivery- Telemetry delivery mechanismembrace-android-config- Configuration management and remote configembrace-android-telemetry-persistence- Disk persistence for telemetry data
embrace-android-otel- OTel Kotlin SDK integration (spans, logs, exporters)
Each captures a specific type of telemetry. These include:
anr, app-exit-info, compose-tap, crash-jvm, crash-ndk, fcm, huc, huc-lite,
network-common, network-status, okhttp, power-save, profiler, startup-trace,
taps, thermal-state, view, webview
Instrumentation modules broadly follow this pattern:
- Implement a data source class that extends framework types from
embrace-android-instrumentation-api - Register with the
InstrumentationRegistryinembrace-android-core - Use
SchemaTypefromembrace-android-instrumentation-schemafor telemetry attributes
embrace-test-common- Common test utilitiesembrace-test-fakes- Shared fake implementations (see Testing section)embrace-android-*-fakes- Module-specific fakes forconfig,delivery,otel,instrumentation-api
embrace-gradle-plugin- Gradle plugin for bytecode instrumentation and uploading R8/Dexguard/SO/JS mapping files for getting readable production stacktracesembrace-gradle-plugin-integration-tests- Gradle TestKit-based integration testsembrace-bytecode-instrumentation-tests- Bytecode instrumentation verificationembrace-lint- Custom Android Lint checks (applied vialintChecks)
All modules use convention plugins instead of duplicating build configuration:
| Plugin | Applies to | What it does |
|---|---|---|
embrace-common-conventions |
All modules | Detekt, compiler settings, JVM target |
embrace-android-conventions |
Android library modules | compileSdk, minSdk, lint, test config, Kotlin compiler options |
embrace-jvm-conventions |
JVM-only modules | JVM compiler settings, test config |
embrace-prod-android-conventions |
Publishable Android modules | Adds publishing + Kover on top of android-conventions |
embrace-prod-jvm-conventions |
Publishable JVM modules | Adds publishing + Kover on top of jvm-conventions |
embrace-public-api-conventions |
Public API modules | Explicit API mode, binary compatibility validator, Dokka |
embrace-publishing-conventions |
All published modules | Maven Central publishing via vanniktech plugin |
When creating a new module, apply the appropriate convention plugin rather than configuring build settings directly.
- Max line length: 140 characters
- Detekt: Zero-tolerance (
maxIssues: 0), auto-correct enabled - Kotlin compiler:
allWarningsAsErrors = true - Android Lint:
warningsAsErrors = true,abortOnError = true - Trailing commas: Preferred on declaration sites and call sites
BracesOnIfStatements: Always required on multiline, consistent on single-lineMandatoryBracesLoops: Always requiredForbiddenImport:android.util.Pairis forbidden (usekotlin.Pair)ElseCaseInsteadOfExhaustiveWhen: Prefer exhaustivewhenexpressionsDataClassShouldBeImmutable/DataClassContainsFunctions: Enforced in**/payload/**packagesUnusedImports: EnforcedSpacingBetweenPackageAndImports: Enforced- Each module may have a
config/detekt/baseline.xmlfor suppressed legacy issues
- All new code must be Kotlin
- Package:
io.embrace.android.embracesdk.internal.*for internal code - Public API classes live under
io.embrace.android.embracesdk(non-internalpackages) - Use
@InternalApiannotation to mark APIs that are internal but technically visible - Prefer
internalvisibility for implementation classes - Public API modules use
kotlin.explicitApi()- all declarations must have explicit visibility - No business logic in payload data classes (enforced by detekt)
- JSON files should not have a trailing newline
- Prefer interfaces rather than concrete classes for public APIs
The SDK uses manual dependency injection via module interfaces:
InitModule -> CoreModule -> EssentialServiceModule -> ...
- Each DI module is defined as an interface in
embrace-android-core/.../injection/ - Implementations are
*Implclasses (e.g.,InitModuleImpl) - Fake implementations exist in
embrace-test-fakesfor testing (e.g.,FakeInitModule) - The
ModuleInitBootstrapperinembrace-android-sdkwires all modules together
- No coroutines in the SDK codebase - concurrency uses
ScheduledExecutorService BackgroundWorker(inembrace-android-infra) wrapsScheduledExecutorServiceto limit API surfaceWorkerThreadModulemanages thread pools- Tests use
FakeWorkersfor deterministic scheduling
- JUnit 4 (not JUnit 5)
- Robolectric for Android framework mocking in unit tests
- MockK for Kotlin mocking
- OkHttp MockWebServer for HTTP testing
Unit tests: src/test/kotlin/ in each module
- Standard JUnit 4 tests with
@Test,@Before, etc. - Use backtick-quoted test names:
`sensitive properties are redacted` - Tests are
internal class
Integration tests: src/integrationTest/kotlin/ in embrace-android-sdk
- Uses
SdkIntegrationTestRule(a JUnitExternalResourcerule) - Boots the full SDK with controlled fakes for time, config, and delivery
- Tests live in
testcases/andtestcases/features/packages - Test framework utilities in
testframework/package
Gradle plugin integration tests: embrace-gradle-plugin-integration-tests
- Uses Gradle TestKit
- Supports remote JVM debugging (see module README)
- Prefer fakes over mocks: The codebase has extensive hand-written fakes in dedicated modules
- Fakes are named
Fake*(e.g.,FakeConfigService,FakeClock,FakePayloadStore) - Module-specific fakes live in
embrace-android-*-fakesmodules - Cross-cutting fakes live in
embrace-test-fakes - Common test utilities in
embrace-test-common - Each module's tests may also have local fakes in
src/test/kotlin/.../fakes/ - Mocks should not be used unless they are unavoidable
unitTests.isReturnDefaultValues = true(Android methods return defaults instead of throwing)unitTests.isIncludeAndroidResources = true- Max parallel forks:
(availableProcessors / 3) + 1 - Max heap: 2g
- Uses AndroidX Test Orchestrator
- Public API modules use the Binary Compatibility Validator
- API dumps are stored in
<module>/api/<module-name>.api - Any change to public API signatures will fail CI until
./gradlew apiDumpis run - Public API modules must have Dokka documentation; build fails on Dokka warnings
internalpackages are suppressed in generated docs
- Version is in
gradle.properties(currently8.2.0-SNAPSHOT) - Published to Maven Central via vanniktech maven-publish plugin
- Group ID:
io.embrace - Artifact IDs match module names (e.g.,
embrace-android-sdk,embrace-android-api) - Snapshot publishing runs daily via CI
- Release workflow:
create-release-branch.yml->pre-release-workflow.yml->upload-artifacts-to-maven-central.yml
- Platform: GitHub Actions
- Main CI (
ci-gradle.yml): Runs on push tomainand all PRs- Runs
./gradlew build(includes compile, lint, detekt, unit tests) - Runs
koverXmlReportfor code coverage - Uploads coverage to Codecov
- Builds example app
- Runs
- Java 21 on CI, uses Depot runners for non-dependabot builds
- Robolectric dependencies are pre-fetched and cached
- Configuration cache is enabled
From CONTRIBUTING.md:
- PRs must have a stated goal and detailed description
- Include test coverage and documentation where applicable
- Pass all CI checks (build, lint, detekt, tests)
- Require at least one approval from a project member
- AI disclosure required: none, autocomplete/research, or mostly AI-generated
- Follow existing code and naming conventions
- Lint suppression must be done in code with explanation
- Commits should be reasonably small (<500 lines diff) with proper messages
- PR template has
GoalandTestingsections
- Create module named
embrace-android-instrumentation-<feature> - Apply
embrace-prod-android-conventionsplugin - Implement a data source extending types from
embrace-android-instrumentation-api - Define schema attributes using
SchemaTypefromembrace-android-instrumentation-schema - Register in the instrumentation registry
- Add to
embrace-android-sdk/build.gradle.ktsas animplementationdependency - Add module to
settings.gradle.kts - Add a
README.mddescribing the module's purpose - Create a fakes module if needed for testing
- Add to
embrace-android-payloadmodule - Use Moshi annotations:
@JsonClass(generateAdapter = true)and@Json(name = "...") - Use
data classwithvalproperties (immutability enforced by detekt) - No business logic in payload classes (enforced by detekt)
- Prefer fakes over mocks (check
embrace-test-fakesfirst) - Use
lateinit var+@Beforesetup pattern - Use descriptive backtick-quoted test names
- For integration tests, use
SdkIntegrationTestRule - Mark test classes as
internal
- Do NOT use
android.util.Pair- it's a forbidden import; usekotlin.Pair - Do NOT add business logic to payload data classes - detekt enforces immutability and no functions
- Do NOT use coroutines - the SDK uses
BackgroundWorker/ScheduledExecutorService - Do NOT bump Compose version past 1.0.5 -
getAllSemanticsNodessignature changed in 1.6+, breaking backward compat - Some dependency versions are pinned due to an unpatched AGP issue (see comments in
libs.versions.toml) - Configuration cache is enabled with
problems=fail- all build logic must be compatible - Public API changes require running
./gradlew apiDumpand committing the updated.apifiles
- Prefer interfaces in public APIs over concrete symbols/constructors
- Minimize the public API surface area
- Use defensive programming and be paranoid with your error checking
- The SDK must not crash. When throwing exceptions make sure that something will catch it that isn't the library consumer.