diff --git a/OneSignalSDK/detekt/detekt-baseline-core.xml b/OneSignalSDK/detekt/detekt-baseline-core.xml
index 3f07ef1a1..690d3de2a 100644
--- a/OneSignalSDK/detekt/detekt-baseline-core.xml
+++ b/OneSignalSDK/detekt/detekt-baseline-core.xml
@@ -174,6 +174,7 @@
InstanceOfCheckForException:HttpClient.kt$HttpClient$t is UnknownHostException
LongMethod:ApplicationService.kt$ApplicationService$override suspend fun waitUntilSystemConditionsAvailable(): Boolean
LongMethod:ConfigModelStoreListener.kt$ConfigModelStoreListener$private fun fetchParams()
+ LongMethod:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome
LongMethod:HttpClient.kt$HttpClient$private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse
LongMethod:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse
LongMethod:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun createUser( createUserOperation: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse
@@ -196,7 +197,6 @@
LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, )
LongParameterList:IDatabase.kt$IDatabase$( table: String, columns: Array<String>? = null, whereClause: String? = null, whereArgs: Array<String>? = null, groupBy: String? = null, having: String? = null, orderBy: String? = null, limit: String? = null, action: (ICursor) -> Unit, )
LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, )
- LongParameterList:IParamsBackendService.kt$ParamsObject$( var googleProjectNumber: String? = null, var enterprise: Boolean? = null, var useIdentityVerification: Boolean? = null, var notificationChannels: JSONArray? = null, var firebaseAnalytics: Boolean? = null, var restoreTTLFilter: Boolean? = null, var clearGroupOnSummaryClick: Boolean? = null, var receiveReceiptEnabled: Boolean? = null, var disableGMSMissingPrompt: Boolean? = null, var unsubscribeWhenNotificationsDisabled: Boolean? = null, var locationShared: Boolean? = null, var requiresUserPrivacyConsent: Boolean? = null, var opRepoExecutionInterval: Long? = null, var influenceParams: InfluenceParamsObject, var fcmParams: FCMParamsObject, )
LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, )
LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, )
LongParameterList:OutcomeEventsController.kt$OutcomeEventsController$( private val _session: ISessionService, private val _influenceManager: IInfluenceManager, private val _outcomeEventsCache: IOutcomeEventsRepository, private val _outcomeEventsPreferences: IOutcomeEventsPreferences, private val _outcomeEventsBackend: IOutcomeEventsBackendService, private val _configModelStore: ConfigModelStore, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _deviceService: IDeviceService, private val _time: ITime, )
@@ -282,8 +282,8 @@
RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e
ReturnCount:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution
ReturnCount:BackgroundManager.kt$BackgroundManager$override fun cancelRunBackgroundServices(): Boolean
- ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean
ReturnCount:ConfigModel.kt$ConfigModel$override fun createModelForProperty( property: String, jsonObject: JSONObject, ): Model?
+ ReturnCount:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome
ReturnCount:HttpClient.kt$HttpClient$private suspend fun makeRequest( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse
ReturnCount:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse
ReturnCount:JSONUtils.kt$JSONUtils$fun compareJSONArrays( jsonArray1: JSONArray?, jsonArray2: JSONArray?, ): Boolean
@@ -295,6 +295,8 @@
ReturnCount:Model.kt$Model$protected fun getOptIntProperty( name: String, create: (() -> Int?)? = null, ): Int?
ReturnCount:Model.kt$Model$protected fun getOptLongProperty( name: String, create: (() -> Long?)? = null, ): Long?
ReturnCount:Model.kt$Model$protected inline fun <reified T : Enum<T>> getOptEnumProperty(name: String): T?
+ ReturnCount:OneSignalImp.kt$OneSignalImp$@Suppress("TooGenericExceptionCaught") private fun internalInit( context: Context, appId: String?, ): Boolean
+ ReturnCount:OneSignalImp.kt$OneSignalImp$@Suppress("TooGenericExceptionCaught") private fun warnIfBlockingOnMainThread(operationName: String?)
ReturnCount:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation?
ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean
ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendAndCreateOutcomeEvent( name: String, weight: Float, // Note: this is optional sessionTime: Long, influences: List<Influence>, ): OutcomeEvent?
@@ -319,22 +321,27 @@
SwallowedException:DeviceService.kt$DeviceService$e: ClassNotFoundException
SwallowedException:DeviceService.kt$DeviceService$e: PackageManager.NameNotFoundException
SwallowedException:JSONUtils.kt$JSONUtils$t: Throwable
+ SwallowedException:OneSignalImp.kt$OneSignalImp$e: RuntimeException
SwallowedException:PermissionsActivity.kt$PermissionsActivity$e: ClassNotFoundException
SwallowedException:PreferencesService.kt$PreferencesService$ex: Exception
+ SwallowedException:PreferencesService.kt$PreferencesService$t: Throwable
SwallowedException:SyncJobService.kt$SyncJobService$e: Exception
SwallowedException:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$t: Throwable
ThrowsCount:OneSignalImp.kt$OneSignalImp$private suspend fun waitUntilInitInternal(operationName: String? = null)
TooGenericExceptionCaught:AndroidUtils.kt$AndroidUtils$e: Throwable
TooGenericExceptionCaught:DeviceUtils.kt$DeviceUtils$t: Throwable
+ TooGenericExceptionCaught:FeatureFlagsRefreshService.kt$FeatureFlagsRefreshService$e: Exception
TooGenericExceptionCaught:HttpClient.kt$HttpClient$e: Throwable
TooGenericExceptionCaught:HttpClient.kt$HttpClient$t: Throwable
TooGenericExceptionCaught:JSONUtils.kt$JSONUtils$t: Throwable
TooGenericExceptionCaught:Logging.kt$Logging$t: Throwable
TooGenericExceptionCaught:OneSignalDispatchers.kt$OneSignalDispatchers$e: Exception
+ TooGenericExceptionCaught:OneSignalImp.kt$OneSignalImp$e: Exception
TooGenericExceptionCaught:OperationRepo.kt$OperationRepo$e: Throwable
TooGenericExceptionCaught:PreferenceStoreFix.kt$PreferenceStoreFix$e: Throwable
TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$e: Throwable
TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$ex: Exception
+ TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$t: Throwable
TooGenericExceptionCaught:SyncJobService.kt$SyncJobService$e: Exception
TooGenericExceptionCaught:ThreadUtils.kt$e: Exception
TooGenericExceptionCaught:TrackGooglePurchase.kt$TrackGooglePurchase$e: Throwable
@@ -413,10 +420,6 @@
UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResponse
UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResult
UndocumentedPublicClass:IOutcomeEvent.kt$IOutcomeEvent
- UndocumentedPublicClass:IParamsBackendService.kt$FCMParamsObject
- UndocumentedPublicClass:IParamsBackendService.kt$IParamsBackendService
- UndocumentedPublicClass:IParamsBackendService.kt$InfluenceParamsObject
- UndocumentedPublicClass:IParamsBackendService.kt$ParamsObject
UndocumentedPublicClass:IPreferencesService.kt$PreferenceOneSignalKeys
UndocumentedPublicClass:IPreferencesService.kt$PreferencePlayerPurchasesKeys
UndocumentedPublicClass:IPreferencesService.kt$PreferenceStores
@@ -549,8 +552,6 @@
UndocumentedPublicFunction:Logging.kt$Logging$@JvmStatic fun warn( message: String, throwable: Throwable? = null, )
UndocumentedPublicFunction:Logging.kt$Logging$fun addListener(listener: ILogListener)
UndocumentedPublicFunction:Logging.kt$Logging$fun removeListener(listener: ILogListener)
- UndocumentedPublicFunction:LoginHelper.kt$LoginHelper$suspend fun login( externalId: String, jwtBearerToken: String? = null, )
- UndocumentedPublicFunction:LogoutHelper.kt$LogoutHelper$fun logout()
UndocumentedPublicFunction:Model.kt$Model$fun <T> setListProperty( name: String, value: List<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, )
UndocumentedPublicFunction:Model.kt$Model$fun <T> setMapModelProperty( name: String, value: MapModel<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, )
UndocumentedPublicFunction:Model.kt$Model$fun <T> setOptListProperty( name: String, value: List<T>?, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, )
@@ -610,9 +611,9 @@
UnusedPrivateMember:OperationRepo.kt$OperationRepo$private val _time: ITime
UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'login'")
UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'logout'")
- UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'login'")
- UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before 'logout'")
+ UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before '$operationName'")
UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("Must call 'initWithContext' before use")
+ UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw initFailureException ?: IllegalStateException("Initialization failed before '$operationName'")
UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.")
diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt
index dfaaa027d..aca355508 100644
--- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt
+++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt
@@ -17,6 +17,7 @@ import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.core.internal.http.impl.OptionalHeaders
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
+import org.json.JSONException
import org.json.JSONObject
internal class ParamsBackendService(
@@ -39,7 +40,23 @@ internal class ParamsBackendService(
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
}
- val responseJson = JSONObject(response.payload!!)
+ // A 2xx response with a non-JSON body (e.g. an intercepting proxy / captive portal /
+ // CDN error page returning HTML) used to surface as an uncaught JSONException on the
+ // IO dispatcher and silently kill the params refresh loop. Convert it into a
+ // BackendException so the caller's existing retry/backoff path handles it like any
+ // other transient backend failure.
+ val payload = response.payload
+ val responseJson =
+ try {
+ JSONObject(payload ?: "")
+ } catch (ex: JSONException) {
+ Logging.warn(
+ "ParamsBackendService.fetchParams: malformed (non-JSON) response payload, will retry. " +
+ "status=${response.statusCode}",
+ ex,
+ )
+ throw BackendException(response.statusCode, payload, response.retryAfterSeconds)
+ }
// Process outcomes params
var influenceParams: InfluenceParamsObject? = null
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/FeatureFlagsBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/FeatureFlagsBackendServiceTests.kt
index c20b933b0..d7c4c1bee 100644
--- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/FeatureFlagsBackendServiceTests.kt
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/FeatureFlagsBackendServiceTests.kt
@@ -105,6 +105,27 @@ class FeatureFlagsBackendServiceTests : FunSpec({
warns[0].entry.contains("""{"errors":["Forbidden"]}""") shouldBe true
}
+ // Companion regression to SDK-4478 on the params endpoint: a 2xx response with a
+ // completely non-JSON body (intercepting proxy like Burp, captive portal, CDN error
+ // HTML, etc.) must NOT throw and must NOT kill the refresh loop. It should fall
+ // through to Unavailable and log a WARN with a body snippet, exactly like the
+ // existing "wrong-shape JSON" path. FeatureFlagsJsonParser.parseSuccessful already
+ // catches Throwable internally; this test pins that contract so nobody regresses it.
+ test("200 with totally non-JSON body (HTML) returns Unavailable and does not throw") {
+ val htmlBody = "
Burpintercepted"
+ val http = mockk()
+ coEvery { http.get(any(), any()) } returns HttpResponse(200, htmlBody)
+
+ FeatureFlagsBackendService(http).fetchRemoteFeatureFlags("appId") shouldBe
+ RemoteFeatureFlagsFetchOutcome.Unavailable
+ val warns =
+ logsForService().filter {
+ it.level == LogLevel.WARN && it.entry.contains("not valid Turbine feature-flags JSON")
+ }
+ warns shouldHaveSize 1
+ warns[0].entry.contains("") shouldBe true
+ }
+
test("body snippet caps long payloads at 200 chars") {
val longBody = "x".repeat(1_000)
val http = mockk()
diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/ParamsBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/ParamsBackendServiceTests.kt
new file mode 100644
index 000000000..ce4d4788f
--- /dev/null
+++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/backend/impl/ParamsBackendServiceTests.kt
@@ -0,0 +1,92 @@
+package com.onesignal.core.internal.backend.impl
+
+import com.onesignal.common.exceptions.BackendException
+import com.onesignal.core.internal.http.HttpResponse
+import com.onesignal.core.internal.http.IHttpClient
+import com.onesignal.debug.LogLevel
+import com.onesignal.debug.internal.logging.Logging
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import io.mockk.coEvery
+import io.mockk.mockk
+
+/**
+ * Regression coverage for SDK-4478: a 2xx response with a non-JSON body (e.g. an
+ * intercepting proxy or captive portal returning HTML) used to surface as an uncaught
+ * [org.json.JSONException] on the IO dispatcher and silently kill the params refresh
+ * loop. [ParamsBackendService.fetchParams] now converts those into [BackendException] so
+ * the caller's existing retry/backoff path handles them like any other transient failure.
+ */
+class ParamsBackendServiceTests : FunSpec({
+ var originalLogLevel: LogLevel = LogLevel.WARN
+
+ beforeEach {
+ // Logging.warn() ultimately routes through android.util.Log, which is not mocked
+ // in plain JVM unit tests. Silence it for the duration of these tests.
+ originalLogLevel = Logging.logLevel
+ Logging.logLevel = LogLevel.NONE
+ }
+
+ afterEach {
+ Logging.logLevel = originalLogLevel
+ }
+
+ test("malformed (HTML) 200 payload throws BackendException instead of JSONException") {
+ val http = mockk()
+ val htmlBody = "Burpintercepted"
+ coEvery { http.get(any(), any()) } returns HttpResponse(200, htmlBody)
+
+ val ex =
+ shouldThrow {
+ ParamsBackendService(http).fetchParams("appId", null)
+ }
+
+ ex.statusCode shouldBe 200
+ ex.response shouldBe htmlBody
+ }
+
+ test("empty 200 payload throws BackendException instead of NullPointerException") {
+ val http = mockk()
+ coEvery { http.get(any(), any()) } returns HttpResponse(200, null)
+
+ val ex =
+ shouldThrow {
+ ParamsBackendService(http).fetchParams("appId", null)
+ }
+
+ ex.statusCode shouldBe 200
+ }
+
+ test("non-2xx response still throws BackendException with the original status code") {
+ val http = mockk()
+ coEvery { http.get(any(), any()) } returns HttpResponse(403, """{"errors":["Forbidden"]}""")
+
+ val ex =
+ shouldThrow {
+ ParamsBackendService(http).fetchParams("appId", null)
+ }
+
+ ex.statusCode shouldBe 403
+ ex.response shouldBe """{"errors":["Forbidden"]}"""
+ }
+
+ test("valid JSON payload is parsed and returns a populated ParamsObject") {
+ val http = mockk()
+ coEvery { http.get(any(), any()) } returns
+ HttpResponse(
+ 200,
+ """{"android_sender_id":"sender-123","enterp":true,"fcm":{"api_key":"k","app_id":"a","project_id":"p"}}""",
+ )
+
+ val params = ParamsBackendService(http).fetchParams("appId", null)
+
+ params shouldNotBe null
+ params.googleProjectNumber shouldBe "sender-123"
+ params.enterprise shouldBe true
+ params.fcmParams.apiKey shouldBe "k"
+ params.fcmParams.appId shouldBe "a"
+ params.fcmParams.projectId shouldBe "p"
+ }
+})
diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt
index d509b9494..47a0f3645 100644
--- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt
+++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt
@@ -175,19 +175,45 @@ internal class WebViewManager(
activity: Activity,
jsonObject: JSONObject,
): Int {
- return try {
- val pageHeight = jsonObject.getJSONObject("rect").getInt("height")
- var pxHeight = ViewUtils.dpToPx(pageHeight)
- Logging.debug("getPageHeightData:pxHeight: $pxHeight")
- val maxPxHeight = getWebViewMaxSizeY(activity)
- if (pxHeight > maxPxHeight) {
- pxHeight = maxPxHeight
- Logging.debug("getPageHeightData:pxHeight is over screen max: $maxPxHeight")
- }
- pxHeight
- } catch (e: JSONException) {
- Logging.error("pageRectToViewHeight could not get page height", e)
- -1
+ // SDK-4494: avoid throw-then-catch on `rect` being absent. The IAM HTML's
+ // `getPageMetaData()` can legitimately return a payload without `rect` for
+ // benign/recoverable reasons (e.g. JS not yet defined when the activity
+ // rotates, custom IAM template, partial metadata). The previous
+ // `getJSONObject("rect")` raised `JSONException` which we caught and logged
+ // at ERROR with a full stack trace, flooding OTel/Datadog with non-actionable
+ // alerts. Use `optJSONObject` and `optInt` so missing fields are a structured
+ // null/sentinel instead, and downgrade the log to a single WARN line.
+ val rect = jsonObject.optJSONObject("rect")
+ val pageHeight = rect?.optInt("height", -1) ?: -1
+ if (pageHeight < 0) {
+ Logging.warn(
+ "pageRectToViewHeight could not get page height (missing/invalid 'rect.height'); " +
+ "snippet=${bodySnippet(jsonObject.toString())}",
+ )
+ return -1
+ }
+ var pxHeight = ViewUtils.dpToPx(pageHeight)
+ Logging.debug("getPageHeightData:pxHeight: $pxHeight")
+ val maxPxHeight = getWebViewMaxSizeY(activity)
+ if (pxHeight > maxPxHeight) {
+ pxHeight = maxPxHeight
+ Logging.debug("getPageHeightData:pxHeight is over screen max: $maxPxHeight")
+ }
+ return pxHeight
+ }
+
+ /**
+ * Trim [body] to a short, single-line snippet safe for logcat / OTel. See
+ * SDK-4494 - we only want enough context to debug shape mismatches without
+ * dumping the full WebView payload into log pipelines.
+ */
+ private fun bodySnippet(body: String?): String {
+ if (body.isNullOrEmpty()) return ""
+ val flattened = body.replace('\n', ' ').replace('\r', ' ')
+ return if (flattened.length <= LOG_BODY_SNIPPET_MAX_CHARS) {
+ flattened
+ } else {
+ flattened.take(LOG_BODY_SNIPPET_MAX_CHARS) + "…"
}
}
@@ -233,6 +259,19 @@ internal class WebViewManager(
}
webView!!.evaluateJavascript(GET_PAGE_META_DATA_JS_FUNCTION) { value ->
+ // SDK-4494: `evaluateJavascript` returns the JSON-encoded result of the
+ // expression. When the JS function is undefined or returns `undefined`
+ // (e.g. WebView not fully loaded yet) the callback receives the literal
+ // string "null", which `JSONObject(...)` rejects. Bail out early instead
+ // of throwing+catching, and route any remaining surprise through Logging
+ // (was previously `e.printStackTrace()`, which bypassed our log pipeline).
+ if (value.isNullOrBlank() || value == "null") {
+ Logging.warn(
+ "calculateHeightAndShowWebViewAfterNewActivity: empty/null page metadata " +
+ "from WebView; skipping height update",
+ )
+ return@evaluateJavascript
+ }
try {
val pagePxHeight = pageRectToViewHeight(activity, JSONObject(value))
@@ -240,7 +279,11 @@ internal class WebViewManager(
showMessageView(pagePxHeight)
}
} catch (e: JSONException) {
- e.printStackTrace()
+ Logging.warn(
+ "calculateHeightAndShowWebViewAfterNewActivity: could not parse page metadata; " +
+ "snippet=${bodySnippet(value)}",
+ e,
+ )
}
}
}
@@ -463,6 +506,12 @@ internal class WebViewManager(
companion object {
private val MARGIN_PX_SIZE = ViewUtils.dpToPx(24)
+
+ // SDK-4494: cap the body snippet included in WARN logs so a malformed/large
+ // WebView payload can't blow up the OTel log entry. Same pattern as
+ // FeatureFlagsBackendService.
+ private const val LOG_BODY_SNIPPET_MAX_CHARS = 200
+
const val JS_OBJ_NAME = "OSAndroid"
const val GET_PAGE_META_DATA_JS_FUNCTION = "getPageMetaData()"
const val SET_SAFE_AREA_INSETS_JS_FUNCTION = "setSafeAreaInsets(%s)"