diff --git a/CHANGELOG.md b/CHANGELOG.md index 2269fe31..9199c57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Match `failedRequestTargets` as regular expressions on Apple, not only as substrings. Cocoa treats plain strings as substrings, so the default `.*` matched no URL and HTTP client errors were never captured on Apple. A target now matches when the URL contains it or when its regex matches anywhere in the URL, matching the JavaScript, Python, Cocoa and Dart SDKs. Apps using the default targets will now capture HTTP client errors (500–599 by default) on Apple. - Fix Apple linker errors introduced in 0.28.0 for `SentryDefaultThreadInspector` and Swift compatibility libraries ([#589](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/589)). ### Dependencies diff --git a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.apple.kt b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.apple.kt index 396b53dc..232e5126 100644 --- a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.apple.kt +++ b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.apple.kt @@ -9,6 +9,7 @@ import io.sentry.kotlin.multiplatform.log.updateFrom import io.sentry.kotlin.multiplatform.setEnableUnhandledCppExceptionMonitoring import kotlinx.cinterop.convert import platform.Foundation.NSNumber +import platform.Foundation.NSRegularExpression internal fun SentryOptions.toCocoaOptionsConfiguration(): (CocoaSentryOptions?) -> Unit = { @@ -101,7 +102,7 @@ internal fun CocoaSentryOptions.applyCocoaBaseOptions(kmpOptions: SentryOptions) } cocoaOptions.setEnableCaptureFailedRequests(kmpOptions.enableCaptureFailedRequests) - cocoaOptions.setFailedRequestTargets(kmpOptions.failedRequestTargets) + cocoaOptions.setFailedRequestTargets(kmpOptions.failedRequestTargets.toCocoaRequestTargets()) cocoaOptions.setFailedRequestStatusCodes( kmpOptions.failedRequestStatusCodes.map { SentryHttpStatusCodeRange( @@ -111,3 +112,17 @@ internal fun CocoaSentryOptions.applyCocoaBaseOptions(kmpOptions: SentryOptions) }, ) } + +/** + * Cocoa matches a plain string target as a substring of the URL and only treats + * NSRegularExpression targets as regexes, so a KMP regex such as the default `.*` would never + * match. Pass each target both ways: the URL contains the target, or the target's regex matches + * anywhere in the URL. Targets that aren't valid regexes match as strings only. + */ +internal fun List.toCocoaRequestTargets(): List = + flatMap { target -> + listOfNotNull( + target, + NSRegularExpression.regularExpressionWithPattern(target, 0u, null), + ) + } diff --git a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/FailedRequestTargetsOptionsTest.kt b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/FailedRequestTargetsOptionsTest.kt new file mode 100644 index 00000000..8115fa81 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/FailedRequestTargetsOptionsTest.kt @@ -0,0 +1,74 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.extensions.toCocoaOptionsConfiguration +import kotlinx.cinterop.convert +import platform.Foundation.NSMakeRange +import platform.Foundation.NSRegularExpression +import platform.Foundation.numberOfMatchesInString +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class FailedRequestTargetsOptionsTest { + private fun cocoaFailedRequestTargets(vararg kmpTargets: String): List<*> { + val nativeOptions = CocoaSentryOptions() + SentryOptions() + .apply { if (kmpTargets.isNotEmpty()) failedRequestTargets = kmpTargets.toList() } + .toCocoaOptionsConfiguration() + .invoke(nativeOptions) + return nativeOptions.failedRequestTargets() + } + + // Mirrors +[SentryTracePropagation isTargetMatch:withTargets:] in sentry-cocoa, which is not + // exposed to Kotlin: an NSString target matches when the URL contains it, an NSRegularExpression + // target when it matches anywhere in the URL. + private fun List<*>.cocoaMatches(url: String): Boolean = + any { target -> + when (target) { + is NSRegularExpression -> + target.numberOfMatchesInString(url, 0u, NSMakeRange(0u, url.length.convert())) > 0u + is String -> url.contains(target) + else -> false + } + } + + @Test + fun `default targets match every URL`() { + val targets = cocoaFailedRequestTargets() + + assertTrue(targets.cocoaMatches("https://example.com/api/items")) + assertTrue(targets.cocoaMatches("http://localhost:8080/")) + } + + @Test + fun `plain string target matches URLs containing it`() { + val targets = cocoaFailedRequestTargets("example.com") + + assertTrue(targets.cocoaMatches("https://api.example.com/items")) + assertFalse(targets.cocoaMatches("https://other.org/items")) + } + + @Test + fun `regex target matches URLs`() { + val targets = cocoaFailedRequestTargets("https://.*\\.example\\.com/.*") + + assertTrue(targets.cocoaMatches("https://api.example.com/items")) + assertFalse(targets.cocoaMatches("https://example.org/items")) + } + + @Test + fun `regex target matches part of a URL`() { + val targets = cocoaFailedRequestTargets("api\\.example\\.(com|org)") + + assertTrue(targets.cocoaMatches("https://api.example.org/items")) + assertFalse(targets.cocoaMatches("https://api.example.net/items")) + } + + @Test + fun `invalid regex target still matches as a plain string`() { + val targets = cocoaFailedRequestTargets("[") + + assertTrue(targets.cocoaMatches("https://example.com/[]")) + assertFalse(targets.cocoaMatches("https://example.com/items")) + } +} diff --git a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryOptions.kt b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryOptions.kt index ec688c79..765f2359 100644 --- a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryOptions.kt +++ b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryOptions.kt @@ -126,7 +126,8 @@ public open class SentryOptions { /** * A list of HTTP request targets indicating which client errors should be captured as errors with - * either regex or a plain string. + * either regex or a plain string. A target matches when the request URL contains it, or when it + * matches anywhere in the URL as a regular expression. * * By default, HTTP client errors from every target (.* regular expression) are automatically captured. *