Read and follow all guidance in CONTRIBUTING.md. Use the documentation map to select current SDK guidance and Foundation validation commands. Follow the module rules and validation tasks below.
Include necessary documentation with each implementation change. Review accuracy, reader purpose, navigation, and instruction scope. Record actual validation evidence. For staged documentation validation, use a provisioned FactoryKit environment:
python ci/scripts/foundation_check.py --snapshot index --against HEAD --format jsonThis manual gate supplements the existing module validation tasks.
dd-sdk-android-core— SDK core:Datadog,InternalLogger, storage/upload, shared executors (CoreFeature).dd-sdk-android-internal— internals shared by several modules (only put code here if more than one module needs it).features/*— product features (RUM, Logs, Trace, Session Replay, Profiling, Flags, NDK, WebView, …).integrations/*— integrations with third-party libraries (OkHttp, Cronet, Compose, Coil, Glide, Apollo, …).reliability/— integration tests;reliability/single-fit/*holds per-feature E2E tests.instrumented/integration— legacy integration test suite.tools/*— custom detekt and lint rules,noopfactorycode generator, shared unit test utilities (tools/unit).sample/*— sample apps (sample/kotlinis the main one).
Load the matching project skill before these tasks. The links below point to the
tracked sources in ai/skills/. The .agents/skills/ and .claude/skills/
directory aliases expose those same sources to agent hosts.
- Writing, editing, or reviewing tests:
sdk-tests. - Reviewing a branch or PR:
sdk-review. - Writing commit messages or PR titles:
sdk-commit. - Running pre-PR checks and regenerating generated files:
sdk-pr-prepare. - Creating or updating stacked PRs with Graphite:
sdk-pr-stack. - Inspecting events emitted by the sample app:
android-sdk-event-inspection.
The root build.gradle.kts registers aggregate tasks that run across all submodules:
./gradlew assembleLibrariesDebug # build all library modules (debug)
./gradlew assembleLibraries # build all library modules (debug + release)
./gradlew unitTestDebug # unit tests, all modules, debug variant
./gradlew unitTestRelease # unit tests, all modules, release variant
./gradlew lintCheckAll # lint all modules
./gradlew checkAll # lint + unit tests + instrumented tests — requires connected device/emulator
./gradlew instrumentTestAll # all instrumented (integration) tests — requires connected device/emulatorcheckAll includes instrumentTestAll, so it fails or hangs without a device. Without a device, run ./gradlew lintCheckAll unitTestAll instead (unitTestAll covers debug, release and tool tests).
Prefer per-module tasks, which are much faster than the aggregate ones:
./gradlew :features:dd-sdk-android-rum:testDebugUnitTest
./gradlew :features:dd-sdk-android-rum:testDebugUnitTest --tests "*RumViewScopeTest*" # single test class
./gradlew :features:dd-sdk-android-rum:testDebugUnitTest --tests "*RumViewScopeTest.M send event*" # single test methodSample app requires a flavor name:
./gradlew :sample:kotlin:assembleUs1Debug # us1 is the default flavorCode formatting:
./gradlew <module_name>:ktlintFormat
# or for all modules, with included build
./gradlew ktlintFormatAllDetekt checks:
./gradlew <module_name>:detekt
# or for all modules
./gradlew detektRun these for every module you changed before considering the work done:
./gradlew :<module>:ktlintFormat
./gradlew :<module>:detekt # only modules applying `detekt-conventions` (not tools/*, sample/*, reliability/*, instrumented/*)
./gradlew :<module>:testDebugUnitTest # Android modules; JVM modules (tools/detekt, tools/lint, tools/noopfactory) use :<module>:test, build-logic uses :build-logic:test
./gradlew checkGeneratedFiles # if public API, dependencies or Kotlin/AAR metadata may have changedEvery publishable module has generated files checked into git. CI fails if they are stale (./gradlew checkGeneratedFiles verifies all of them). Regenerate with the per-module task:
| File | Update task |
|---|---|
api/apiSurface |
./gradlew :<module>:generateApiSurface |
api/<module>.api |
./gradlew :<module>:apiDump |
api/compiler-meta.txt |
./gradlew :<module>:generateCompilerMetadata |
api/aar-metadata.txt |
./gradlew :<module>:generateAarMetadataInfo |
transitiveDependencies |
./gradlew :<module>:generateTransitiveDependenciesList |
LICENSE-3rdparty.csv at the repo root is verified by checkDependencyLicensesAll. Add an entry for each new third-party dependency, keeping the file in alphabetical order.
Some modules generate Kotlin data classes from JSON schemas at build time (from src/main/json/). The generated Kotlin files land in build/generated/<generationTaskName>/. Do not edit them directly.
Some schema folders are cloned from the upstream rum-events-format repo and are overwritten on sync:
features/dd-sdk-android-rum/src/main/json/rumandjson/telemetry(cloneRumSchema,cloneTelemetrySchema)features/dd-sdk-android-profiling/src/main/json/profilingfeatures/dd-sdk-android-session-replay/src/main/json/schemas
Changes to these must be made upstream in rum-events-format first, then synced. The other schema folders (e.g. dd-sdk-android-core/src/main/json/rc, features/dd-sdk-android-logs/src/main/json/log, features/dd-sdk-android-trace/src/main/json/trace, features/dd-sdk-android-flags/src/main/json/flags) are local and can be edited directly. Rebuild after editing.
These are rules that reviewers repeatedly enforce. Follow them when writing code to avoid review round-trips, and flag violations of them when reviewing a PR.
Exception for shared SDK internals: dd-sdk-android-internal is an implementation dependency of core and features; its cross-module types may be public (including data classes) without being customer API. Changes are allowed without customer-facing deprecation. Before changing signatures or removing members, check usages in SDK modules and cross-platform repositories (dd-sdk-reactnative, dd-sdk-flutter, dd-sdk-unity).
- Keep everything
internalunless it is meant for customers. Check theapi/apiSurfacediff for anything leaked by accident. - Don't expose
Implclasses, internal types in public signatures, or internal classes in public KDoc. Cross-module internal access goes through_RumInternalProxy-style proxies or a cast inside the function body. - If a class is meant to be built via
Builder, make its constructorinternalorprivate. - Return empty collections instead of nullable collections. Prefer a NoOp implementation over a nullable type. Avoid unneeded default arguments.
- Changing a public signature, nullability or a data class shape is a breaking change. Flag it explicitly and prefer deprecate-and-alias. Also consider cross-platform consumers such as Flutter, React Native and KMP.
- Avoid
data classin public API unless needed (copybecomes part of the ABI). - New public APIs should be consistent with the iOS SDK. If you can't verify iOS parity, say so in the PR description as an open question.
- Don't create new threads or executors, if possible. Prefer to reuse existing ones such as
contextExecutorServiceorpersistenceExecutorServiceinCoreFeature. UsesubmitSafe/executeSafefromConcurrencyExt.kt. - Don't block the main thread or SDK initialization.
getFeatureContexttakes a lock. On hot paths, subscribe withFeatureContextUpdateReceiverinstead. - Don't stack concurrency primitives, e.g. an
AtomicBooleanorConcurrentHashMapinsidesynchronized. Prefer immutable data plus a single atomic swap (getAndSet) to multiple layers of locking. - Declare
AtomicXXXfields asval, notvar: reassigning the reference bypasses the atomic. - Multiple SDK core instances can coexist. Don't keep mutable state in
objects or companion objects; scope it to theSdkCoreinstance, so that it doesn't leak between instances or get delivered to only one of them. If a feature holds shared state, add a test with two cores.
- Log only via
InternalLogger, obtained from the relevant SDK core. Never useandroid.util.Log,printlnorSystem.errin SDK code. The sample app uses Timber. - Choose the target deliberately, following the rule of thumb in the
InternalLoggerKDoc.USERis for messages that are actionable or report the main data-processing steps (tracking, storage, upload).TELEMETRYis for events we need to monitor.MAINTAINERis for internal details and is visible only in SDK debug builds. UseonlyOnce = truefor messages that could spam: log and telemetry spam costs customers money and hides real problems. - Pass the exception as the
throwableargument. Don't pute.messagein the log text, since the logger already prints it. - Never throw from SDK code paths that could reach the host app. Catch narrow exception types (e.g.
IOException), notThrowableorException. Exception to this rule: calls into customer-provided or third-party code (event mappers, serializers, listeners) whose exceptions are unknown: there, catchingThrowableis intentional (seeSerializer.serializeToByteArray); suppressTooGenericExceptionCaughtat that site and log the exception. Don't swallow exceptions silently. Don't create a bareThrowableorException; use a specific subclass.
MIN_SDKis 23. Guard newer APIs withBuildSdkVersionProvider(it is annotated with@ChecksSdkIntAtLeast). Don't use@SuppressLint("NewApi"): it hides crashes on older API levels, whileBuildSdkVersionProvidercan be mocked in tests to cover both branches.- Android and third-party calls that can throw must be declared in
detekt_custom_unsafe_calls.ymland handled. Only genuinely non-throwing calls go into thedetekt_custom_safe_calls_*.ymlfiles. Keep these files sorted. TheUnsafeThirdPartyFunctionCallrule skips the SDK's owncom.datadog.*calls, so handle exceptions thrown by SDK code explicitly; listing them in these files has no effect. - Place
@Suppress(...)on the narrowest scope: the call site, not the whole method or class. - Use
Locale.USfor case conversions and formatting. The default locale gives wrong results in some locales (e.g. Turkish dotlessi). - Dependencies go in
gradle/libs.versions.toml. Preferimplementationoverapi. A new module must be registered insettings.gradle.kts. To make it publishable, apply themaven-publishandsigningplugins and callapplyPublishingConfig(...)in itsdatadogBuild { }block (seeintegrations/dd-sdk-android-cronet/build.gradle.kts). CI publishing (publishToSonatype) picks it up automatically. - Prefer JSON schemas and generated models over hand-written JSON parsing (see Generated Models).
- Don't put code into
dd-sdk-android-internalif only one module uses it.
- Use imports instead of fully qualified names.
- Order class members as: properties, overridden/interface methods, then private methods. Put
companion objectand nestedBuilderat the bottom. Mark the companioninternalorprivatewhen possible. - Don't add comments or KDoc that restate the code. Use
@inheritDocinstead of copy-pasting KDoc. Keep existing comments accurate when changing code. - Names must be precise. For example, use
setrather thanaddfor a single value. Don't use names that clash with existing concepts, such as "elapsed" vselapsedRealtime. Don't putThreadin the name of something that isn't a thread. - Prefer the type system: sealed classes or enums over strings,
T : AnyoverT?, Kotlin function types overjava.util.function.*.
Before creating, editing, or reviewing test code, load and follow the project sdk-tests skill from .agents/skills/sdk-tests/SKILL.md.
Unit test conventions (class boilerplate, naming, test rules, Elmyr Forge configurators) live in the sdk-tests skill.
instrumented/integration, reliability/core-it and reliability/with-backend contain instrumented tests that run on a connected device or emulator.
reliability/single-fit/* contains E2E tests that run on the JVM without a device, e.g. ./gradlew :reliability:single-fit:rum:testReleaseUnitTest (this is what CI runs). The Flags module also has a privacy suite that runs on a connected device or emulator: ./gradlew :reliability:single-fit:flags:connectedReleaseAndroidTest. Its README documents the exact-AAR option.
./gradlew instrumentTestAll # all integration tests
./gradlew :instrumented:integration:connectedDebugAndroidTest # legacy integration suite
./gradlew :reliability:core-it:connectedDebugAndroidTest # core integration testsThe sample app reads credentials from gitignored JSON files in config/. Missing files don't break the build (empty strings are used), but the app won't send data to Datadog. Schema (from build-logic/.../SampleAppConfig.kt):
{
"token": "",
"rumApplicationId": "",
"apiKey": "",
"applicationKey": "",
"logsEndpoint": "",
"tracesEndpoint": "",
"rumEndpoint": "",
"sessionReplayEndpoint": "",
"remoteConfigurationId": ""
}Filename matches the flavor: config/us1.json, config/staging.json, etc. Get credentials from your Datadog org.
To inspect the events the SDK emits from the sample app, use the android-sdk-event-inspection skill (.claude/skills/android-sdk-event-inspection).
- Default branch for PRs:
develop - Branch name format:
<username>/<ticket>/<short-description>, e.g.nogorodnikov/rum-18858/replace-argthat-with-captor. - Commit messages and PR titles: follow the
sdk-commitskill.
- Use the project PR template (
.github/PULL_REQUEST_TEMPLATE.md). - Keep PRs focused. Don't include unrelated refactors, formatting churn, debug leftovers, stray files or "may be needed later" code (unused properties, schemas, files).
- Don't edit
CHANGELOG.mdin feature PRs. It is updated only in release preparation PRs. - When preparing a release changelog: tags reflect customer impact, not change size. Internal-only changes are
[MAINTENANCE], not[IMPROVEMENT]. Follow the existing entry format:* [TAG] Description. See [#N](link). - When reviewing a release preparation PR (version bump and
CHANGELOG.mdupdate for a new release), review only the changes the PR itself makes. The release includes changes from PRs that were already reviewed and merged, so don't flag breaking changes or binary incompatibilities that come from them (e.g. a public API diff against the previous release): those were approved in their own PRs.
Follow the contributor handoff guidance
and the additional agent requirements below when checking whether to prompt the
user to add ready-for-human-review. Check at these checkpoints:
- After opening a PR, pushing changes to it, or marking it ready for review.
- After replying to or resolving the remaining agentic feedback.
- When an agentic review finishes and you inspect its result.
- When the user asks about PR status or readiness for human review.
After opening a PR, pushing changes, or marking a draft ready, agents must allow at least five minutes for asynchronous reviews to start before assessing eligibility. Measure from the latest of these actions and restart the grace period if the head changes. The grace period alone does not establish eligibility.
Prompt for the label only when all of these conditions are met:
- At least one agentic or bot review has completed against the current PR head. No feedback is not evidence that a review occurred; a review of an older revision does not qualify.
- No observed agentic reviews remain queued or running. If an expected review has not appeared, eligibility remains unconfirmed.
- All agentic feedback has been addressed or explicitly dispositioned with evidence, each feedback item has a posted reply, and all associated review threads are resolved, including outdated threads.
Refresh the current PR head, review activity, feedback, and thread status. Check all pages of results; if retrieval fails or is incomplete, eligibility remains unconfirmed. Local fixes, draft replies, or a clean bot summary alone are not sufficient. Do not resolve unaddressed feedback solely to qualify for the label.
If eligible and the label is absent, ask: "All agentic feedback has been addressed.
Shall I add ready-for-human-review?" Apply the label only after user confirmation,
then recheck the current head and eligibility immediately before labelling. If the
label is already present, do not prompt or add it again.
If eligibility is unconfirmed, report what remains pending. If the task ends before the grace period or reviews complete, report the deferred check; do not claim readiness or imply that monitoring will continue without a separately arranged follow-up.
This instruction does not independently authorise posting replies, resolving threads, pushing, or merging; follow the applicable approval requirements for those actions. Carry this rule into delegated PR work and PR automations.
dogfooding is managed only through the scripts in ci/scripts/dogfooding/; read ci/scripts/dogfooding/README.md before helping with it. Never push to dogfooding, open a PR from a feature branch into it, or merge into it with squash or rebase: use feature.sh, sync.sh and reset.sh, which open the PRs. The only change made by hand is reverting a feature's dogfood merges, as described in the README's "Remove a single feature". Show the command and ask before running the scripts, since they push branches and open PRs.