Merge develop into feature/continuous-profiling#3573
Conversation
Merge develop into feature/heatmaps
…eal/PANA-5681/view-identity-resolver
…ntity-resolver PANA-5681: Add `HeatmapIdentifier` infrastructure [1 of 4]
…pdates PANA-5681: Update RUM and Session Replay schemas for heatmaps [2 of 4]
…maps PANA-5681: Integrate HeatmapIdentifierRegistry into RUM for heatmaps [3 of 4]
…olors RUM-8112: Fix gradients not showing in compose
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…ection-issues RUM-16551: Fix hidden API reflection noise in Session Replay drawable mappers
Update the CHANGELOG and switch AndroidConfig version from SNAPSHOT to Release. Co-authored-by: Cursor <[email protected]>
…replay-heatmaps PANA-5681: Integrate HeatmapIdentifierRegistry into Session Replay for heatmaps [4 of 4]
Co-authored-by: Cursor <[email protected]>
PANA-5681: Add heatmaps feature flag
RUM-16882: Fix the cronet sample crashing when loading 404.png
…util Remove the unused KotlinDSLUtils from the Gradle build
The analysis:detekt-custom job runs custom Detekt rules by invoking ./gradlew customDetektRules, which sequentially executes 31 independent JavaExec tasks — one JVM per module. Because the tasks share no state and declare no inter-dependencies, they are safe to run concurrently. Adding --parallel --max-workers=4 reduces step 6 of the job from ~155s to ~69s (2.2× speedup) on a local benchmark of all 31 modules. Each worker JVM is capped at maxHeapSize=2g to bound heap usage. The job's Kubernetes memory limit is increased to 32Gi (matching datadog-android) to accommodate 4 concurrent JVMs: 4 × ~3.25Gi (heap + metaspace) + 4Gi Gradle daemon ≈ 18Gi, well within the new limit. # Conflicts: # ci/pipelines/default-pipeline.yml
assembleLibrariesRelease is the dominant step in analysis:detekt-custom, taking ~8m 17s (56% of the job) in CI. Adding --parallel enables Gradle to compile independent library modules concurrently within the same daemon process (in-process workers sharing the 4Gi heap) rather than sequentially. Unlike customDetektRules --parallel which spawns separate JVMs per task, --parallel on assembleLibrariesRelease uses Gradle's built-in worker infrastructure — no additional memory per concurrent task. The module dependency graph has a natural fan-out: dd-sdk-android-internal and dd-sdk-android-core must compile first, then 25+ feature and integration modules can compile in parallel.
Move KUBERNETES_MEMORY_LIMIT from 13Gi to 32Gi in the root .gitlab-ci.yml, matching datadog-android which uses the same limit on the same runner pool. The higher limit is needed for jobs that run parallel JVM workers (analysis:detekt-custom, test:debug). Setting it globally is simpler than per-job overrides and has no scheduling cost since KUBERNETES_MEMORY_REQUEST remains at 8Gi — the scheduler still only needs 8Gi free on the node. Actual memory consumed per job is unchanged for jobs that don't use the extra headroom. Removes the now-redundant KUBERNETES_MEMORY_LIMIT: 32Gi override from analysis:detekt-custom.
…--parallel test:debug ran 5 separate ./gradlew --no-daemon invocations sequentially. Each call pays the full Gradle startup and project configuration overhead (~10-15s in CI × 4 extra calls = ~40-60s wasted). Consolidating into a single invocation eliminates that redundancy. --parallel --max-workers=4 lets Gradle run up to 4 modules' test tasks concurrently. Each module forks its own test JVM (~768MB); 4 concurrent test JVMs + 3Gi Gradle JVM ≈ 6Gi total — well within the 32Gi limit now set globally. Note: unlike assembleLibrariesRelease --parallel (in-process workers, shared heap), test tasks fork separate JVMs. --max-workers=4 acts as the memory safety valve, the same role it plays for customDetektRules.
…--parallel test:kover ran 4 separate ./gradlew --no-daemon invocations for coverage generation, paying ~33s of Gradle startup overhead across 3 extra calls. Consolidating into a single invocation using the >- YAML folded scalar eliminates that overhead (same fix applied to test:debug). --parallel --max-workers=4 lets Gradle run up to 4 modules' koverXmlReport tasks concurrently. koverXmlReportRelease forks a test JVM per module for instrumented coverage tracking (~1.5-2GB each); 4 workers stay well within the 32Gi global limit set in the previous commit. test:kover is the current critical path of the test stage (~17m 55s avg) now that test:debug has been reduced to ~7m 57s.
…analysis-optimization RUM-14050: Reduce CI pipeline time via parallelism and Gradle consolidation
Fix regression of missing tinted backgrounds
…rossplatform-prep [PANA-5681] Preparation for Heatmaps cross-platform integration
Merge develop into feature/heatmaps
Merge heatmaps into develop
a12149e to
463955c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 463955cfcf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| lastPublishedScreenName = context.viewUrl | ||
| lastPublishedEntries.clear() | ||
| lastPublishedEntries.putAll(context.entries) | ||
| registry.setHeatmapIdentifiers( |
There was a problem hiding this comment.
Retry publishing after RUM becomes available
When Session Replay records its first heatmap snapshot before RUM is registered, LazyHeatmapIdentifierRegistry.setHeatmapIdentifiers() is a no-op because getFeature(RUM) returns null. These lines still mark that snapshot as already published, so later traversals of the same unchanged hierarchy see hierarchyChanged == false and never retry after RUM registers; heatmap permanent IDs stay absent until the UI hierarchy changes. This affects apps that enable/start Session Replay before registering RUM or delay RUM initialization.
Useful? React with 👍 / 👎.
| if (context.entries.isEmpty()) { | ||
| lastPublishedScreenName = null | ||
| lastPublishedEntries.clear() |
There was a problem hiding this comment.
Clear the registry when a traversal has no targets
If a previous traversal published identifiers and a later traversal on the same RUM view has no heatmap entries, this branch only clears the resolver's local cache and never calls registry.setHeatmapIdentifiers(emptyMap(), context.viewUrl). The shared HeatmapIdentifierStore therefore keeps serving the old snapshot for that view URL, so subsequent RUM lookups can attach stale permanent IDs after the tracked targets are removed or dropped from Session Replay.
Useful? React with 👍 / 👎.
| } | ||
| val resolvedColor = resolveGradientFillColor(drawable) | ||
| val colorFilter = resolveGradientColorFilter(drawable) | ||
| if (resolvedColor == null || colorFilter == null) return null |
There was a problem hiding this comment.
Preserve solid GradientDrawable colors without filters
For a plain XML <shape> / GradientDrawable with a solid fill, drawable.color is non-null but drawable.colorFilter is normally null. Returning null here makes ViewWireframeMapper skip the shape style, so those backgrounds produce no shape wireframe on API 29+; the mirrored N+ branch does the same on API 24-28. The previous path returned the fill color when no filter was present, so only filtered/tinted drawables should need the fallback behavior.
Useful? React with 👍 / 👎.
| internal val systemRequirementsConfiguration: SystemRequirementsConfiguration, | ||
| internal val internalCallback: SessionReplayInternalCallback | ||
| internal val internalCallback: SessionReplayInternalCallback, | ||
| internal val heatmapsEnabled: Boolean |
There was a problem hiding this comment.
Preserve SessionReplayConfiguration binary ABI
Because SessionReplayConfiguration is a public data class, adding this primary-constructor property changes the generated JVM signatures for copy, copy$default, and component methods. Apps or libraries compiled against the previous SDK that call those generated methods can hit NoSuchMethodError after a dependency-only upgrade to this version; the builder flag should be stored without changing the data-class constructor ABI or with compatibility overloads.
Useful? React with 👍 / 👎.
| heatmapContext = heatmapContext | ||
| ) | ||
|
|
||
| heatmapContext?.publish() |
There was a problem hiding this comment.
Aggregate heatmap identifiers across all roots
When more than one decor root is captured, WindowsOnDrawListener calls produce() once per root, and this publish replaces the registry snapshot each time. That leaves identifiers only for the last root in the z-ordered list, so taps on views in another visible window such as an Activity behind a Dialog/Popup are recorded without a matching heatmap permanent ID even though their wireframes are included in the same Session Replay snapshot.
Useful? React with 👍 / 👎.
| val resolvedColor = resolveGradientFillColor(drawable) | ||
| if (resolvedColor == null || drawable.colorFilter == null) return null | ||
| val colorAlpha = (resolvedColor ushr ALPHA_SHIFT_ANDROID) and MAX_ALPHA_VALUE | ||
| val fillAlpha = (colorAlpha * drawable.alpha) / MAX_ALPHA_VALUE | ||
| return if (fillAlpha == 0) null else mergeColorAndAlpha(resolvedColor, fillAlpha) |
There was a problem hiding this comment.
Apply GradientDrawable color filters on API 24-28
On API 24-28 the default drawable mapper enters this N+ branch for GradientDrawables. If the drawable has an explicit colorFilter, the branch only checks that the filter exists and then serializes resolvedColor, so tinted/filtered shape backgrounds are replayed with their original fill color rather than the color actually drawn on screen. The old reflection path read the PorterDuffColorFilter color for these API levels.
Useful? React with 👍 / 👎.
What does this PR do?
Merge develop into feature/continuous-profiling
Review checklist (to be filled by reviewers)