Move SSD client diagnostics JSON to typed Kotlin

ober

33777fe61d7886664fd23c1025b69d21cf32fd15

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 6ebdb59..62b8d57 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -3512,7 +3512,9 @@
     (typed-kotlin-file "com/sfb/ssdreview/ClientLogJson.kt"
       (kotlin-imports (org json JSONObject))
       (typed-library (com sfb ssdreview)
-        (export clientLogPayloadJson)
+        (export clientLogPayloadJson clientLogCrashFields
+                clientLogFallbackPayload appLogPackageInfo appLogVersionInfo
+                deviceLogInfoJson throwableLogJson statusLogFields)
         (type JSONObject)
         (type Int)
         (type Int32)
@@ -3549,6 +3551,55 @@
                 (clientLogPdfJson pdfName page pageCount))
               (json-object-put-string! out "selected-group" selectedGroupId)
               (json-object-put-json-object! out "fields" fields)
+              out)))
+        (def (clientLogCrashFields (threadName : String) (error : JSONObject)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-string! out "crashing-thread" threadName)
+              (json-object-put-json-object! out "error" error)
+              out)))
+        (def (clientLogFallbackPayload (event : String)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-string! out "event" event)
+              out)))
+        (def (appLogPackageInfo (packageName : String)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-string! out "package" packageName)
+              out)))
+        (def (appLogVersionInfo (packageName : String)
+                                (versionName : String)
+                                (versionCode : Int)) : JSONObject
+          (let ((out (appLogPackageInfo packageName)))
+            (begin
+              (json-object-put-string! out "version-name" versionName)
+              (json-object-put-int! out "version-code" versionCode)
+              out)))
+        (def (deviceLogInfoJson (sdk : Int32)
+                                (release : String)
+                                (manufacturer : String)
+                                (model : String)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-int32! out "sdk" sdk)
+              (json-object-put-string! out "release" release)
+              (json-object-put-string! out "manufacturer" manufacturer)
+              (json-object-put-string! out "model" model)
+              out)))
+        (def (throwableLogJson (className : String)
+                               (message : String)
+                               (stack : String)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-string! out "class" className)
+              (json-object-put-string! out "message" message)
+              (json-object-put-string! out "stack" stack)
+              out)))
+        (def (statusLogFields (text : String)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-string! out "status" text)
               out)))))
 
     (typed-kotlin-file "com/sfb/ssdreview/TruthIndexSelect.kt"
@@ -5496,11 +5547,9 @@
        "            val payload = try {"
        "                clientLogPayload("
        "                    \"ssd-uncaught-exception\","
-       "                    JSONObject()"
-       "                        .put(\"crashing-thread\", crashingThread.name ?: \"\")"
-       "                        .put(\"error\", throwableLog(throwable))"
+       "                    clientLogCrashFields(crashingThread.name ?: \"\", throwableLog(throwable))"
        "                )"
-       "            } catch (_: Exception) { JSONObject().put(\"event\", \"ssd-uncaught-exception\") }"
+       "            } catch (_: Exception) { clientLogFallbackPayload(\"ssd-uncaught-exception\") }"
        "            try {"
        "                val sender = thread(start = true) { sendClientLog(payload, CLIENT_LOG_CRASH_TIMEOUT_MS) }"
        "                sender.join((CLIENT_LOG_CRASH_TIMEOUT_MS + 500).toLong())"
@@ -5574,38 +5623,37 @@
        "        )"
        ""
        "    private fun appLogInfo(): JSONObject {"
-       "        val out = JSONObject().put(\"package\", packageName)"
+       "        val out = appLogPackageInfo(packageName)"
        "        return try {"
        "            val info = packageManager.getPackageInfo(packageName, 0)"
        "            val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else {"
        "                @Suppress(\"DEPRECATION\")"
        "                info.versionCode.toLong()"
        "            }"
-       "            out.put(\"version-name\", info.versionName ?: \"\").put(\"version-code\", versionCode)"
+       "            appLogVersionInfo(packageName, info.versionName ?: \"\", versionCode)"
        "        } catch (_: Exception) { out }"
        "    }"
        ""
        "    private fun deviceLogInfo(): JSONObject ="
-       "        JSONObject()"
-       "            .put(\"sdk\", Build.VERSION.SDK_INT)"
-       "            .put(\"release\", Build.VERSION.RELEASE ?: \"\")"
-       "            .put(\"manufacturer\", Build.MANUFACTURER ?: \"\")"
-       "            .put(\"model\", Build.MODEL ?: \"\")"
+       "        deviceLogInfoJson("
+       "            Build.VERSION.SDK_INT,"
+       "            Build.VERSION.RELEASE ?: \"\","
+       "            Build.MANUFACTURER ?: \"\","
+       "            Build.MODEL ?: \"\""
+       "        )"
        ""
        "    private fun throwableLog(error: Throwable): JSONObject {"
        "        val writer = StringWriter()"
        "        error.printStackTrace(PrintWriter(writer))"
        "        val stack = writer.toString()"
-       "        return JSONObject()"
-       "            .put(\"class\", error.javaClass.name)"
-       "            .put(\"message\", error.message ?: \"\")"
-       "            .put(\"stack\", if (stack.length <= CLIENT_LOG_STACK_CHARS) stack else stack.take(CLIENT_LOG_STACK_CHARS) + \"...[truncated]\")"
+       "        val stackText = if (stack.length <= CLIENT_LOG_STACK_CHARS) stack else stack.take(CLIENT_LOG_STACK_CHARS) + \"...[truncated]\""
+       "        return throwableLogJson(error.javaClass.name, error.message ?: \"\", stackText)"
        "    }"
        ""
        "    private fun setStatus(text: String) {"
        "        status.text = text"
        "        if (isErrorStatus(text)) {"
-       "            logClientEvent(\"ssd-status-error\", JSONObject().put(\"status\", text))"
+       "            logClientEvent(\"ssd-status-error\", statusLogFields(text))"
        "        }"
        "    }"
        ""