Repository navigation
refactor(android): replace kotlinx.serialization with org.json parsing and JsonByteWriter - #787
Merged
Merged
Conversation
…inx.serialization Co-authored-by: Cursor <cursoragent@cursor.com>
…rialization Port the Android client SDK's JsonByteWriter to Kotlin as a plain JSON writer (no LDValue/EvaluationReason or Gson HTML-safe mode) and use it for every payload the SDK sends: OTLP traces/metrics/logs, rrweb replay events, GraphQL requests and the persisted Session Replay failure record. Output matches what kotlinx.serialization produced byte for byte. Co-authored-by: Cursor <cursoragent@cursor.com>
abelonogov-ld
enabled auto-merge (squash)
October 2, 2026 17:46
abelonogov-ld
added a commit
that referenced
this pull request
Oct 6, 2026
…#791) ## Problem A customer (Delta Faucet) cannot build a MAUI app for `net10.0-android` in Release with `LaunchDarkly.SessionReplay`. R8 rejects two classes as duplicates: - `okio/-DeflaterSinkExtensions` - `kotlinx.serialization.json.Json$Default` Both come from `ldobserve-otel-bundle.jar`, the fat JAR we ship inside the package. The app acquires its own copy of the same classes through unrelated bindings — `Square.OkIO` arrives transitively via `AndroidX.DataStore`, which Firebase depends on — and two definitions of a class is a hard failure for R8. It only surfaces in Release, because Debug does not run R8. This is not new to any particular OTel version. The released 0.10.2 bundle already contains 433 unshaded `okio/` and `okhttp3/` entries, including the exact class R8 names. ## Fix **okio/okhttp** are relocated under `com/launchdarkly/sessionreplay/shaded/` by `BundleOtelJarsTask`, using an ASM `ClassRemapper`. They cannot simply be dropped: the OTLP exporters resolve their transport through `ServiceLoader`, the only `HttpSenderProvider` we register is the OkHttp one, and OkHttp's entire I/O layer is Okio. Removing either produces the "No HttpSenderProvider found on classpath" crash at runtime. Relocating makes the collision impossible instead of depending on the app's dependency graph staying clear. The remapper also rewrites string constants, not just bytecode class references. OkHttp builds the path to its public-suffix resource by concatenating the literal `"okhttp3/internal/publicsuffix/"` with a simple class name, so a remapper that only moved class references would relocate the resource and leave the lookup pointing at the old path. **kotlinx.serialization** is already gone from `observability-android` as of #787, which replaced it with `org.json`. This PR drops the now-unused `Org.Jetbrains.Kotlinx.KotlinxSerializationJson` package references from the binding and the fat package. That reference was the other half of the duplicate-class failure: its targets file hardcoded a path into `$(Home)/.gradle/caches/...`. ## Also in this PR - **`MauiSample10.csproj`** — a .NET 10 variant of the sample, sharing sources with `MauiSample9.csproj`. `global.json` is removed so both SDK bands resolve. Not added to the `.sln`. - **Firebase** is a permanent dependency of the .NET 10 sample. It is the shortest path to reproducing this class of bug (`Firebase.Common` → `AndroidX.DataStore.Preferences` → `Square.OkIO.JVM`), so it stays as a standing compatibility check rather than a one-off repro. - **`NoWarn` for NU1605/NU1608 removed** from the sample, so dependency regressions are visible instead of silent. - **`Analytics(pageViews = false)` → `Analytics(screenViews = false)`** in the MAUI Kotlin bridge. `main`'s `ObservabilityOptions.Analytics` has had `screenViews` for a while; the MAUI bridge was still passing `pageViews` and would not compile against it. The MAUI native Gradle build is not in CI, which is why this went unnoticed. - **Lifecycle floors stay at 2.9.2.1**, with a comment explaining why. Bumping them to 2.11.0.1 forces SavedState 1.5 / Collection 1.6 on consumers and turns our own SavedState and Collection floors into NU1605 downgrade errors. - **Navigation bar title** in the sample now shows the configuration and target framework (Debug/Release, .NET 9/10). - **README** brought back in line with the shipped API: corrected the automatic-crash-reporting claim, updated prerequisites, documented explicit `ObservabilityPlugin` registration, and named both sample projects. ## Split out: the OTel upgrade This branch originally also upgraded to OTel Java 1.66 and OTel Android 1.7. That has been moved to `feat/otel-1.7-upgrade`. OTel Android 1.7 pulls `androidx.core` 1.19, `androidx.lifecycle` 2.11 and `okhttp-android` 5.4, all of which declare a minimum compileSdk of 36–37 and a minimum AGP of 9.1. Our e2e app (compileSdk 35, AGP 8.13.2) fails `checkComposeDebugAarMetadata` on them, and so would every native Android consumer still on AGP 8. The `resolutionStrategy` pins that make it build locally only apply inside our own Gradle build; they are not published, so consumers see the raw transitives. Since the relocation fix is independent of the OTel version, there is no reason to hold the customer fix behind an AGP 9 migration. ## Verification - Bundle JAR at OTel 1.51: **0** unshaded `okio/`/`okhttp3/` entries, **433** relocated under the shaded prefix. `-DeflaterSinkExtensions` is relocated; the `publicsuffixes.gz` resource moves with its database class. - `META-INF/services` intact after the merge; `HttpSenderProvider` still resolves to `OkHttpHttpSenderProvider`. - Clean rebuild of the native module and the Android binding in Release resolves OTel back to 0.11.0-alpha, androidx.core 1.16, lifecycle 2.8.7. - `MauiSample10` builds for `net10.0-android` in **Release** with Firebase present: R8 passes, signed APK and AAB produced, 0 errors. This is the customer's failing configuration. - No NU1605 in the sample restore. Nine NU1608 warnings remain, all from MAUI 10 / Firebase pulling AndroidX base packages ahead of their `.Ktx` siblings; they are not caused by this package and are unchanged from `main`'s pins. ## Not verified OTLP export through the relocated OkHttp has not been exercised on a device. The SPI registration and the public-suffix resource are checked statically only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Fixes **.NET 10 Android Release** builds that fail R8 duplicate-class checks when `LaunchDarkly.SessionReplay` is used alongside common deps (e.g. Firebase → DataStore → OkIO). > > **Android OTel bundle:** `BundleOtelJarsTask` now **relocates `okio` and `okhttp3`** under `com.launchdarkly.sessionreplay.shaded/` via ASM (classes, resources, and string constants such as OkHttp’s public-suffix paths). OkHttp/Okio stay in the bundle for OTLP `HttpSenderProvider`, but no longer collide with app copies. **`Org.Jetbrains.Kotlinx.KotlinxSerializationJson`** is removed from the Android binding and fat package to avoid the second duplicate (`kotlinx.serialization.json.Json$Default`). > > **MAUI bridge:** `ObservabilityOptions.Analytics` uses **`screenViews = false`** (was obsolete `pageViews`) so Android navigation isn’t double-counted with MAUI screen reporting. > > **Samples & docs:** Adds **`MauiSample10.csproj`** (shared sources with .NET 9), **Firebase** on the .NET 10 Android sample as a standing Release R8 check, **`HttpActivityPropagationSupport`** on both samples, and **README** updates (.NET 9/10, HTTP tracing, plugin registration, crash reporting). Package version **0.11.3**; **`global.json`** removed for multi-SDK builds. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 43a4eeb. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes
kotlinx-serialization-json(and the Kotlin serialization compiler plugin) from the Android Observability SDK.org.json.JSONObject. All payloads are small, whereorg.jsonis faster than a reflection-based mapper and adds no dependency.JsonByteWriterfrom the Android client SDK (andrey/event-encoding-byte-writer) writes compact UTF-8 JSON in a single pass. It is stripped of theLDValue/EvaluationReasonhelpers and the Gson HTML-safe mode. It is used for OTLP traces/metrics/logs, rrweb replay events, GraphQL request bodies, and the failure record.OtlpJsonEncoding.ktandReplayEventEncoding.kt.Behaviour notes:
\u2028/\u2029. Both forms are valid JSON.GraphQLClientandOtlpHttpClientlose thejsonconstructor parameter; GraphQL variables areMap<String, Any?>;OtlpHttpClient.sendtakes aByteArray;CustomEventDataWrapper.datais aMap;identifyReplaySession'suserObjectisMap<String, String>?; serializer objects and the unusedErrorObjectInputare removed.How did you test this change?
JsonByteWriterTest(56 tests: structure, numbers, escaping, UTF-8/surrogates, optional fields, buffer growth, random round trip). The client SDK branch had no standalone writer tests to copy, so these are new.OtlpJsonEncodingTest,ReplayEventEncodingTest, GraphQL request body tests inGraphQLClientTest, and response parsing tests (ReplaySessionResponseParsingTest,SamplingResponseTest)../gradlew :lib:testDebugUnitTest: 537 tests, 0 failures. Release sources compile with no warnings.Are there any deployment considerations?
None. Removes a runtime dependency; no wire-format changes beyond the U+2028/U+2029 escaping noted above.
Note
Overview
Removes
kotlinx-serialization-jsonand the Kotlin serialization Gradle plugin from observability-android, shrinking dependencies and avoiding compiler-plugin serialization.Adds an internal
JsonByteWriterthat writes compact UTF-8 JSON in one pass, plus dedicated encoders for OTLP (OtlpJsonEncoding.kt) and rrweb replay (ReplayEventEncoding.kt). OTLP/replay wire models become plain data classes without@Serializableor customKSerializersurrogates.Parsing for GraphQL responses (sampling, Session Replay) and the persisted replay failure blob moves to
org.json.JSONObjectwith smallfromJsonhelpers andJsonObjectExtensions; JVM unit tests pull inorg.json:jsonbecauseandroid.jaronly stubs it.Call-site shifts:
OtlpHttpClient.sendaccepts pre-encodedByteArray; exporters calltoJsonBytes()on adapter-built requests.GraphQLClientbuilds requests viaJsonByteWriter, takesMap<String, Any?>variables, and parsesdatawith adataParser: (JSONObject) -> T. Replay custom event payloads and GraphQL variables use maps instead ofJsonElement.Tests add broad coverage for the writer, exact OTLP/rrweb JSON output, GraphQL bodies, and response parsing; existing adapter tests now assert against encoded bytes.
Reviewed by Cursor Bugbot for commit 71b4423. Bugbot is set up for automated code reviews on this repo. Configure here.