Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Changelog

## Unreleased

### 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
{
Expand Down Expand Up @@ -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(
Expand All @@ -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<String>.toCocoaRequestTargets(): List<Any> =
flatMap { target ->
listOfNotNull(
target,
NSRegularExpression.regularExpressionWithPattern(target, 0u, null),
)
}
Original file line number Diff line number Diff line change
@@ -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"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading