Skip to content

Latest commit

 

History

History
271 lines (197 loc) · 18.4 KB

AGENTS.md

File metadata and controls

271 lines (197 loc) · 18.4 KB

Start here

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 json

This manual gate supplements the existing module validation tasks.

Module Map

  • 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, noopfactory code generator, shared unit test utilities (tools/unit).
  • sample/* — sample apps (sample/kotlin is the main one).

Agent Skills

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.

Build Commands

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/emulator

checkAll 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 method

Sample app requires a flavor name:

./gradlew :sample:kotlin:assembleUs1Debug   # us1 is the default flavor

Code formatting:

./gradlew <module_name>:ktlintFormat

# or for all modules, with included build
./gradlew ktlintFormatAll

Detekt checks:

./gradlew <module_name>:detekt

# or for all modules
./gradlew detekt

Before You Finish

Run 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 changed

Checked-in Generated Files

Every 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.

Generated Models

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/rum and json/telemetry (cloneRumSchema, cloneTelemetrySchema)
  • features/dd-sdk-android-profiling/src/main/json/profiling
  • features/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.

Code Conventions

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.

Public API

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 internal unless it is meant for customers. Check the api/apiSurface diff for anything leaked by accident.
  • Don't expose Impl classes, 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 constructor internal or private.
  • 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 class in public API unless needed (copy becomes 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.

Threading

  • Don't create new threads or executors, if possible. Prefer to reuse existing ones such as contextExecutorService or persistenceExecutorService in CoreFeature. Use submitSafe/executeSafe from ConcurrencyExt.kt.
  • Don't block the main thread or SDK initialization. getFeatureContext takes a lock. On hot paths, subscribe with FeatureContextUpdateReceiver instead.
  • Don't stack concurrency primitives, e.g. an AtomicBoolean or ConcurrentHashMap inside synchronized. Prefer immutable data plus a single atomic swap (getAndSet) to multiple layers of locking.
  • Declare AtomicXXX fields as val, not var: 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 the SdkCore instance, 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.

Logging and Errors

  • Log only via InternalLogger, obtained from the relevant SDK core. Never use android.util.Log, println or System.err in SDK code. The sample app uses Timber.
  • Choose the target deliberately, following the rule of thumb in the InternalLogger KDoc. USER is for messages that are actionable or report the main data-processing steps (tracking, storage, upload). TELEMETRY is for events we need to monitor. MAINTAINER is for internal details and is visible only in SDK debug builds. Use onlyOnce = true for messages that could spam: log and telemetry spam costs customers money and hides real problems.
  • Pass the exception as the throwable argument. Don't put e.message in 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), not Throwable or Exception. Exception to this rule: calls into customer-provided or third-party code (event mappers, serializers, listeners) whose exceptions are unknown: there, catching Throwable is intentional (see Serializer.serializeToByteArray); suppress TooGenericExceptionCaught at that site and log the exception. Don't swallow exceptions silently. Don't create a bare Throwable or Exception; use a specific subclass.

Static Analysis and Compatibility

  • MIN_SDK is 23. Guard newer APIs with BuildSdkVersionProvider (it is annotated with @ChecksSdkIntAtLeast). Don't use @SuppressLint("NewApi"): it hides crashes on older API levels, while BuildSdkVersionProvider can be mocked in tests to cover both branches.
  • Android and third-party calls that can throw must be declared in detekt_custom_unsafe_calls.yml and handled. Only genuinely non-throwing calls go into the detekt_custom_safe_calls_*.yml files. Keep these files sorted. The UnsafeThirdPartyFunctionCall rule skips the SDK's own com.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.US for case conversions and formatting. The default locale gives wrong results in some locales (e.g. Turkish dotless i).
  • Dependencies go in gradle/libs.versions.toml. Prefer implementation over api. A new module must be registered in settings.gradle.kts. To make it publishable, apply the maven-publish and signing plugins and call applyPublishingConfig(...) in its datadogBuild { } block (see integrations/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-internal if only one module uses it.

Code Style

  • Use imports instead of fully qualified names.
  • Order class members as: properties, overridden/interface methods, then private methods. Put companion object and nested Builder at the bottom. Mark the companion internal or private when possible.
  • Don't add comments or KDoc that restate the code. Use @inheritDoc instead of copy-pasting KDoc. Keep existing comments accurate when changing code.
  • Names must be precise. For example, use set rather than add for a single value. Don't use names that clash with existing concepts, such as "elapsed" vs elapsedRealtime. Don't put Thread in the name of something that isn't a thread.
  • Prefer the type system: sealed classes or enums over strings, T : Any over T?, Kotlin function types over java.util.function.*.

Testing

Before creating, editing, or reviewing test code, load and follow the project sdk-tests skill from .agents/skills/sdk-tests/SKILL.md.

Unit Tests

Unit test conventions (class boilerplate, naming, test rules, Elmyr Forge configurators) live in the sdk-tests skill.

Integration Tests

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 tests

Sample App Config

The 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).

Branches and Commits

  • 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-commit skill.

Pull Requests

  • 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.md in 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.md update 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.

Handoff to human review

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 Branch

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.