From 653ed95f14b4b6006747742cb8111f09cd223c6e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 11 Jun 2026 00:01:13 -0400 Subject: [PATCH] fix(android): always serialize event `type` so plays aren't dropped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native client's /api/events requests omitted the `type` discriminator entirely. The app's Json is configured with encodeDefaults=false (AppModule), so a `type` left at its data-class default ("play_started" etc.) is never written to the wire. The server multiplexes on `type` and returns 400 "unknown event type" for an empty one, which PlayEventsReporter's catch swallows — and the play_started path has no offline fallback, so the play is lost with no trace. Net effect: EVERY native Android play event (started/ended/skipped/ offline) has 400'd since this code was written. Listening History only ever populated from the Flutter/web clients; as usage moved to the native app, History went sparse. Confirmed live in the server access log: POST /api/events -> 400 on every play, while reads 200. Force the discriminator onto the wire with @EncodeDefault(Mode.ALWAYS) on each request type's `type` field. Surgical (vs flipping encodeDefaults globally), and idiomatic for a constant-valued discriminator. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../minstrel/models/wire/EventsWire.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt index f901b11b..74d34d96 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt @@ -1,5 +1,9 @@ +@file:OptIn(ExperimentalSerializationApi::class) + package com.fabledsword.minstrel.models.wire +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -15,7 +19,11 @@ import kotlinx.serialization.Serializable @Serializable data class PlayStartedRequest( - val type: String = "play_started", + // Discriminator MUST stay on the wire. The app's Json has + // encodeDefaults=false, so without this a `type` left at its + // default is omitted entirely and the server 400s the event as + // "unknown event type" — silently dropping every native play. + @EncodeDefault(EncodeDefault.Mode.ALWAYS) val type: String = "play_started", @SerialName("track_id") val trackId: String, @SerialName("client_id") val clientId: String, val source: String? = null, @@ -28,21 +36,21 @@ data class PlayStartedResponse( @Serializable data class PlayEndedRequest( - val type: String = "play_ended", + @EncodeDefault(EncodeDefault.Mode.ALWAYS) val type: String = "play_ended", @SerialName("play_event_id") val playEventId: String, @SerialName("duration_played_ms") val durationPlayedMs: Long, ) @Serializable data class PlaySkippedRequest( - val type: String = "play_skipped", + @EncodeDefault(EncodeDefault.Mode.ALWAYS) val type: String = "play_skipped", @SerialName("play_event_id") val playEventId: String, @SerialName("position_ms") val positionMs: Long, ) @Serializable data class PlayOfflineRequest( - val type: String = "play_offline", + @EncodeDefault(EncodeDefault.Mode.ALWAYS) val type: String = "play_offline", @SerialName("track_id") val trackId: String, @SerialName("client_id") val clientId: String, val at: String,