Skip to content

refactor(android): replace kotlinx.serialization with org.json parsing and JsonByteWriter - #787

Merged
abelonogov-ld merged 3 commits into
mainfrom
feat/android-parse-json-with-org-json
Oct 2, 2026
Merged

abelonogov-ld merged 3 commits into
mainfrom
feat/android-parse-json-with-org-json

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Removes kotlinx-serialization-json (and the Kotlin serialization compiler plugin) from the Android Observability SDK.

  • Parsing: GraphQL responses (sampling config, Session Replay init/identify/push) and the persisted Session Replay failure record are parsed with Android's platform org.json.JSONObject. All payloads are small, where org.json is faster than a reflection-based mapper and adds no dependency.
  • Writing: a Kotlin port of JsonByteWriter from the Android client SDK (andrey/event-encoding-byte-writer) writes compact UTF-8 JSON in a single pass. It is stripped of the LDValue/EvaluationReason helpers and the Gson HTML-safe mode. It is used for OTLP traces/metrics/logs, rrweb replay events, GraphQL request bodies, and the failure record.
  • OTLP and replay models are now plain data classes; encoding lives in OtlpJsonEncoding.kt and ReplayEventEncoding.kt.

Behaviour notes:

  • Output is byte-identical to the previous kotlinx output (same field order, same rules for omitting null/default/empty fields, 64-bit OTLP ints as strings), with one exception: U+2028/U+2029 are now escaped as \u2028/\u2029. Both forms are valid JSON.
  • Internal-facing API changes (no callers outside the lib): GraphQLClient and OtlpHttpClient lose the json constructor parameter; GraphQL variables are Map<String, Any?>; OtlpHttpClient.send takes a ByteArray; CustomEventDataWrapper.data is a Map; identifyReplaySession's userObject is Map<String, String>?; serializer objects and the unused ErrorObjectInput are removed.

How did you test this change?

  • New 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.
  • New exact-output tests: OtlpJsonEncodingTest, ReplayEventEncodingTest, GraphQL request body tests in GraphQLClientTest, and response parsing tests (ReplaySessionResponseParsingTest, SamplingResponseTest).
  • Before the switch, recorded real request bodies (OTLP for every resource/scope combination, replay events, GraphQL, failure record) from the kotlinx implementation and confirmed the new code produces identical bytes. Those temporary comparison tests were removed.
  • ./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-json and the Kotlin serialization Gradle plugin from observability-android, shrinking dependencies and avoiding compiler-plugin serialization.

Adds an internal JsonByteWriter that 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 @Serializable or custom KSerializer surrogates.

Parsing for GraphQL responses (sampling, Session Replay) and the persisted replay failure blob moves to org.json.JSONObject with small fromJson helpers and JsonObjectExtensions; JVM unit tests pull in org.json:json because android.jar only stubs it.

Call-site shifts: OtlpHttpClient.send accepts pre-encoded ByteArray; exporters call toJsonBytes() on adapter-built requests. GraphQLClient builds requests via JsonByteWriter, takes Map<String, Any?> variables, and parses data with a dataParser: (JSONObject) -> T. Replay custom event payloads and GraphQL variables use maps instead of JsonElement.

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.

abelonogov-ld and others added 2 commits October 1, 2026 18:41
…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
abelonogov-ld requested a review from a team as a code owner October 2, 2026 02:23

@Vadman97 Vadman97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@abelonogov-ld
abelonogov-ld enabled auto-merge (squash) October 2, 2026 17:46
@abelonogov-ld
abelonogov-ld merged commit 8740c61 into main Oct 2, 2026
26 checks passed
@abelonogov-ld
abelonogov-ld deleted the feat/android-parse-json-with-org-json branch October 2, 2026 17:51
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants