diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9f0c91..3244cd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- feat: beforeSend / fingerprinting ([#70](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/70)) - feat: configuring http client errors for Apple targets ([#76](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/76)) - feat: improve Objc/Swift experience with @HiddenFromObjc ([#62](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/62)) - feat: add view hierarchy ([#53](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/53)) diff --git a/gradle.properties b/gradle.properties index 74b5b04b..b15d8482 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,4 @@ # Kotlin -kotlin.version=1.8.0 kotlin.incremental.multiplatform=true kotlin.code.style=official kotlin.mpp.stability.nowarn=true diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt new file mode 100644 index 00000000..45c14ff9 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt @@ -0,0 +1,57 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.extensions.toKmpBreadcrumb +import io.sentry.kotlin.multiplatform.extensions.toKmpMessage +import io.sentry.kotlin.multiplatform.extensions.toKmpSentryException +import io.sentry.kotlin.multiplatform.extensions.toKmpSentryLevel +import io.sentry.kotlin.multiplatform.extensions.toKmpUser +import io.sentry.kotlin.multiplatform.protocol.Message +import io.sentry.kotlin.multiplatform.protocol.SentryException +import io.sentry.kotlin.multiplatform.protocol.SentryId +import io.sentry.kotlin.multiplatform.protocol.User + +public actual class SentryEvent actual constructor() : SentryBaseEvent() { + public actual var level: SentryLevel? = null + public actual var message: Message? = null + public actual var logger: String? = null + public actual var fingerprint: MutableList = mutableListOf() + public actual var exceptions: MutableList = mutableListOf() + public override var release: String? = null + public override var environment: String? = null + public override var platform: String? = null + public override var user: User? = null + public override var serverName: String? = null + public override var dist: String? = null + + public constructor(cocoaSentryEvent: CocoaSentryEvent) : this() { + eventId = SentryId(cocoaSentryEvent.eventId.toString()) + level = cocoaSentryEvent.level?.toKmpSentryLevel() + message = cocoaSentryEvent.message?.toKmpMessage() + logger = cocoaSentryEvent.logger + release = cocoaSentryEvent.releaseName + environment = cocoaSentryEvent.environment + platform = cocoaSentryEvent.platform + user = cocoaSentryEvent.user?.toKmpUser() + serverName = cocoaSentryEvent.serverName + dist = cocoaSentryEvent.dist + + val cocoaFingerprint = + cocoaSentryEvent.fingerprint()?.toMutableList() as? MutableList + val cocoaSentryExceptions = + cocoaSentryEvent.exceptions?.map { (it as CocoaSentryException).toKmpSentryException() } + ?.toMutableList() + val cocoaContexts = + cocoaSentryEvent.context?.mapKeys { it.key as String }?.mapValues { it.value as Any } + val cocoaBreadcrumbs = cocoaSentryEvent.breadcrumbs?.mapNotNull { it as? CocoaBreadcrumb } + ?.map { it.toKmpBreadcrumb() }?.toMutableList() + val cocoaTags = + cocoaSentryEvent.tags?.mapKeys { it.key as String }?.mapValues { it.value as String } + ?.toMutableMap() + + cocoaFingerprint?.let { fingerprint = it } + cocoaSentryExceptions?.let { exceptions = it } + cocoaContexts?.let { contexts = it } + cocoaBreadcrumbs?.let { breadcrumbs = it } + cocoaTags?.let { tags = it } + } +} diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt index 1024be21..a85825f9 100644 --- a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt @@ -2,8 +2,11 @@ package io.sentry.kotlin.multiplatform import cocoapods.Sentry.SentryAttachment import cocoapods.Sentry.SentryBreadcrumb +import cocoapods.Sentry.SentryEvent +import cocoapods.Sentry.SentryException import cocoapods.Sentry.SentryId import cocoapods.Sentry.SentryLevel +import cocoapods.Sentry.SentryMessage import cocoapods.Sentry.SentryOptions import cocoapods.Sentry.SentryScope import cocoapods.Sentry.SentryUser @@ -17,3 +20,6 @@ internal typealias CocoaSentryId = SentryId internal typealias CocoaSentryLevel = SentryLevel internal typealias CocoaAttachment = SentryAttachment internal typealias CocoaUserFeedback = SentryUserFeedback +internal typealias CocoaSentryEvent = SentryEvent +internal typealias CocoaMessage = SentryMessage +internal typealias CocoaSentryException = SentryException diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/MessageExtensions.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/MessageExtensions.kt new file mode 100644 index 00000000..c605f902 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/MessageExtensions.kt @@ -0,0 +1,19 @@ +package io.sentry.kotlin.multiplatform.extensions + +import io.sentry.kotlin.multiplatform.CocoaMessage +import io.sentry.kotlin.multiplatform.protocol.Message + +internal fun CocoaMessage.toKmpMessage() = Message( + message = message, + params = params as? List, + formatted = formatted +) + +internal fun Message.toCocoaMessage(): CocoaMessage { + val scope = this@toCocoaMessage + val cocoaMessage = scope.formatted?.let { CocoaMessage(it) } ?: CocoaMessage() + return cocoaMessage.apply { + message = scope.message + params = scope.params + } +} diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryEventExtensions.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryEventExtensions.kt new file mode 100644 index 00000000..94674881 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryEventExtensions.kt @@ -0,0 +1,22 @@ +package io.sentry.kotlin.multiplatform.extensions + +import cocoapods.Sentry.SentryId +import io.sentry.kotlin.multiplatform.CocoaSentryEvent +import io.sentry.kotlin.multiplatform.SentryEvent + +internal fun CocoaSentryEvent.applyKmpEvent(kmpEvent: SentryEvent): CocoaSentryEvent { + kmpEvent.level?.let { level = it.toCocoaSentryLevel() } + kmpEvent.platform?.let { platform = it } + message = kmpEvent.message?.toCocoaMessage() + logger = kmpEvent.logger + fingerprint = kmpEvent.fingerprint + releaseName = kmpEvent.release + environment = kmpEvent.environment + user = kmpEvent.user?.toCocoaUser() + serverName = kmpEvent.serverName + dist = kmpEvent.dist + breadcrumbs = kmpEvent.breadcrumbs?.map { it.toCocoaBreadcrumb() }?.toMutableList() + tags = kmpEvent.tags?.toMutableMap() + eventId = SentryId(kmpEvent.eventId.toString()) + return this +} diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryExceptionExtensions.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryExceptionExtensions.kt new file mode 100644 index 00000000..8308d12d --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryExceptionExtensions.kt @@ -0,0 +1,11 @@ +package io.sentry.kotlin.multiplatform.extensions + +import io.sentry.kotlin.multiplatform.CocoaSentryException +import io.sentry.kotlin.multiplatform.protocol.SentryException + +internal fun CocoaSentryException.toKmpSentryException() = SentryException( + type = type, + value = value, + module = module, + threadId = threadId as Long? +) diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt index 3268eab5..c245a51e 100644 --- a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt @@ -1,10 +1,11 @@ package io.sentry.kotlin.multiplatform.extensions import PrivateSentrySDKOnly.Sentry.PrivateSentrySDKOnly -import cocoapods.Sentry.SentryEvent import cocoapods.Sentry.SentryHttpStatusCodeRange import io.sentry.kotlin.multiplatform.BuildKonfig +import io.sentry.kotlin.multiplatform.CocoaSentryEvent import io.sentry.kotlin.multiplatform.CocoaSentryOptions +import io.sentry.kotlin.multiplatform.SentryEvent import io.sentry.kotlin.multiplatform.SentryOptions import io.sentry.kotlin.multiplatform.nsexception.dropKotlinCrashEvent import kotlinx.cinterop.convert @@ -19,19 +20,19 @@ internal fun SentryOptions.toCocoaOptionsConfiguration(): (CocoaSentryOptions?) * This avoids code duplication for init on iOS. */ internal fun CocoaSentryOptions.applyCocoaBaseOptions(options: SentryOptions) { - this.dsn = options.dsn - this.attachStacktrace = options.attachStackTrace - this.dist = options.dist + dsn = options.dsn + attachStacktrace = options.attachStackTrace + dist = options.dist options.environment?.let { - this.environment = it + environment = it } - this.releaseName = options.release - this.debug = options.debug - this.sessionTrackingIntervalMillis = options.sessionTrackingIntervalMillis.convert() - this.enableAutoSessionTracking = options.enableAutoSessionTracking - this.maxAttachmentSize = options.maxAttachmentSize.convert() - this.maxBreadcrumbs = options.maxBreadcrumbs.convert() - this.beforeSend = { event -> + releaseName = options.release + debug = options.debug + sessionTrackingIntervalMillis = options.sessionTrackingIntervalMillis.convert() + enableAutoSessionTracking = options.enableAutoSessionTracking + maxAttachmentSize = options.maxAttachmentSize.convert() + maxBreadcrumbs = options.maxBreadcrumbs.convert() + beforeSend = { event -> val cocoaName = BuildKonfig.SENTRY_COCOA_PACKAGE_NAME val cocoaVersion = BuildKonfig.SENTRY_COCOA_VERSION @@ -49,14 +50,20 @@ internal fun CocoaSentryOptions.applyCocoaBaseOptions(options: SentryOptions) { sdk?.set("packages", packages) event?.sdk = sdk - dropKotlinCrashEvent(event as NSExceptionSentryEvent?) as SentryEvent? + + val modifiedEvent = event?.let { SentryEvent(it) }?.let { unwrappedEvent -> + val result = options.beforeSend?.invoke(unwrappedEvent) + result?.let { event.applyKmpEvent(it) } + } + + dropKotlinCrashEvent(modifiedEvent as NSExceptionSentryEvent?) as CocoaSentryEvent? } val sdkName = options.sdk?.name ?: BuildKonfig.SENTRY_KMP_COCOA_SDK_NAME val sdkVersion = options.sdk?.version ?: BuildKonfig.VERSION_NAME PrivateSentrySDKOnly.setSdkName(sdkName, sdkVersion) - this.beforeBreadcrumb = { cocoaBreadcrumb -> + beforeBreadcrumb = { cocoaBreadcrumb -> cocoaBreadcrumb?.toKmpBreadcrumb() ?.let { options.beforeBreadcrumb?.invoke(it) }?.toCocoaBreadcrumb() } diff --git a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt index baa2bba8..b2af2f82 100644 --- a/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt +++ b/sentry-kotlin-multiplatform/src/commonAppleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt @@ -13,9 +13,8 @@ internal fun User.toCocoaUser() = CocoaUser().apply { internal fun CocoaUser.toKmpUser() = User().apply { val scope = this@toKmpUser - id = scope.userId.toString() - username = scope.username.toString() - email = scope.email.toString() - ipAddress = scope.ipAddress.toString() - setData(scope.data) + id = scope.userId + username = scope.username + email = scope.email + ipAddress = scope.ipAddress } diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt new file mode 100644 index 00000000..db467e75 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt @@ -0,0 +1,43 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.extensions.toKmpBreadcrumb +import io.sentry.kotlin.multiplatform.extensions.toKmpMessage +import io.sentry.kotlin.multiplatform.extensions.toKmpSentryException +import io.sentry.kotlin.multiplatform.extensions.toKmpSentryLevel +import io.sentry.kotlin.multiplatform.extensions.toKmpUser +import io.sentry.kotlin.multiplatform.protocol.Message +import io.sentry.kotlin.multiplatform.protocol.SentryException +import io.sentry.kotlin.multiplatform.protocol.SentryId +import io.sentry.kotlin.multiplatform.protocol.User + +public actual class SentryEvent actual constructor() : SentryBaseEvent() { + public actual var level: SentryLevel? = null + public actual var message: Message? = null + public actual var logger: String? = null + public actual var fingerprint: MutableList = mutableListOf() + public actual var exceptions: MutableList = mutableListOf() + public override var release: String? = null + public override var environment: String? = null + public override var platform: String? = null + public override var user: User? = null + public override var serverName: String? = null + public override var dist: String? = null + + public constructor(jvmSentryEvent: JvmSentryEvent) : this() { + eventId = SentryId(jvmSentryEvent.eventId.toString()) + level = jvmSentryEvent.level?.toKmpSentryLevel() + message = jvmSentryEvent.message?.toKmpMessage() + logger = jvmSentryEvent.logger + release = jvmSentryEvent.release + environment = jvmSentryEvent.environment + platform = jvmSentryEvent.platform + user = jvmSentryEvent.user?.toKmpUser() + serverName = jvmSentryEvent.serverName + dist = jvmSentryEvent.dist + contexts = jvmSentryEvent.contexts + jvmSentryEvent.fingerprints?.let { fingerprint = it } + jvmSentryEvent.exceptions?.let { exceptions = it.map { it.toKmpSentryException() }.toMutableList() } + jvmSentryEvent.breadcrumbs?.let { breadcrumbs = it.map { it.toKmpBreadcrumb() }.toMutableList() } + jvmSentryEvent.tags?.let { tags = it } + } +} diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt index ddd3373b..68164e6e 100644 --- a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/TypeAliases.kt @@ -3,9 +3,13 @@ package io.sentry.kotlin.multiplatform import io.sentry.Attachment import io.sentry.Breadcrumb import io.sentry.Scope +import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.UserFeedback +import io.sentry.protocol.Contexts +import io.sentry.protocol.Message +import io.sentry.protocol.SentryException import io.sentry.protocol.SentryId import io.sentry.protocol.User @@ -17,3 +21,7 @@ internal typealias JvmSentryId = SentryId internal typealias JvmSentryOptions = SentryOptions internal typealias JvmAttachment = Attachment internal typealias JvmUserFeedback = UserFeedback +internal typealias JvmSentryEvent = SentryEvent +internal typealias JvmMessage = Message +internal typealias JvmSentryException = SentryException +internal typealias JvmContexts = Contexts diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/MessageExtensions.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/MessageExtensions.kt new file mode 100644 index 00000000..07c5d7c5 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/MessageExtensions.kt @@ -0,0 +1,17 @@ +package io.sentry.kotlin.multiplatform.extensions + +import io.sentry.kotlin.multiplatform.JvmMessage +import io.sentry.kotlin.multiplatform.protocol.Message + +internal fun JvmMessage.toKmpMessage() = Message( + message = message, + params = params, + formatted = formatted +) + +internal fun Message.toJvmMessage() = JvmMessage().apply { + val scope = this@toJvmMessage + message = scope.message + params = scope.params + formatted = scope.formatted +} diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryEventExtensions.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryEventExtensions.kt new file mode 100644 index 00000000..ca228ced --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryEventExtensions.kt @@ -0,0 +1,22 @@ +package io.sentry.kotlin.multiplatform.extensions + +import io.sentry.kotlin.multiplatform.JvmSentryEvent +import io.sentry.kotlin.multiplatform.JvmSentryId +import io.sentry.kotlin.multiplatform.SentryEvent + +internal fun JvmSentryEvent.applyKmpEvent(kmpEvent: SentryEvent): JvmSentryEvent { + level = kmpEvent.level?.toJvmSentryLevel() + message = kmpEvent.message?.toJvmMessage() + logger = kmpEvent.logger + fingerprints = kmpEvent.fingerprint + release = kmpEvent.release + environment = kmpEvent.environment + platform = kmpEvent.platform + user = kmpEvent.user?.toJvmUser() + serverName = kmpEvent.serverName + dist = kmpEvent.dist + breadcrumbs = kmpEvent.breadcrumbs?.map { it.toJvmBreadcrumb() } + eventId = JvmSentryId(kmpEvent.eventId.toString()) + tags = kmpEvent.tags + return this +} diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryExceptionExtensions.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryExceptionExtensions.kt new file mode 100644 index 00000000..7fdfcef2 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryExceptionExtensions.kt @@ -0,0 +1,11 @@ +package io.sentry.kotlin.multiplatform.extensions + +import io.sentry.kotlin.multiplatform.JvmSentryException +import io.sentry.kotlin.multiplatform.protocol.SentryException + +internal fun JvmSentryException.toKmpSentryException() = SentryException( + type = type, + value = value, + module = module, + threadId = threadId +) diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt index c525ba70..7275cca7 100644 --- a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.kt @@ -2,21 +2,25 @@ package io.sentry.kotlin.multiplatform.extensions import io.sentry.kotlin.multiplatform.BuildKonfig import io.sentry.kotlin.multiplatform.JvmSentryOptions +import io.sentry.kotlin.multiplatform.SentryEvent import io.sentry.kotlin.multiplatform.SentryOptions internal fun SentryOptions.toJvmSentryOptionsCallback(): (JvmSentryOptions) -> Unit = { it.applyJvmBaseOptions(this) // Apply JVM specific options - it.sdkVersion?.name = this.sdk?.name ?: BuildKonfig.SENTRY_KMP_JAVA_SDK_NAME - it.sdkVersion?.version = this.sdk?.version ?: BuildKonfig.VERSION_NAME + it.sdkVersion?.name = sdk?.name ?: BuildKonfig.SENTRY_KMP_JAVA_SDK_NAME + it.sdkVersion?.version = sdk?.version ?: BuildKonfig.VERSION_NAME - this.sdk?.packages?.forEach { sdkPackage -> + sdk?.packages?.forEach { sdkPackage -> it.sdkVersion?.addPackage(sdkPackage.name, sdkPackage.version) } if (it.sdkVersion?.packages?.none { it.name == BuildKonfig.SENTRY_JAVA_PACKAGE_NAME } == true) { - it.sdkVersion?.addPackage(BuildKonfig.SENTRY_JAVA_PACKAGE_NAME, BuildKonfig.SENTRY_JAVA_VERSION) + it.sdkVersion?.addPackage( + BuildKonfig.SENTRY_JAVA_PACKAGE_NAME, + BuildKonfig.SENTRY_JAVA_VERSION + ) } } @@ -25,18 +29,23 @@ internal fun SentryOptions.toJvmSentryOptionsCallback(): (JvmSentryOptions) -> U * This avoids code duplication during init on Android */ internal fun JvmSentryOptions.applyJvmBaseOptions(options: SentryOptions) { - this.dsn = options.dsn - this.isAttachThreads = options.attachThreads - this.isAttachStacktrace = options.attachStackTrace - this.dist = options.dist - this.environment = options.environment - this.release = options.release - this.isDebug = options.debug - this.sessionTrackingIntervalMillis = options.sessionTrackingIntervalMillis - this.isEnableAutoSessionTracking = options.enableAutoSessionTracking - this.maxAttachmentSize = options.maxAttachmentSize - this.maxBreadcrumbs = options.maxBreadcrumbs - this.setBeforeBreadcrumb { jvmBreadcrumb, _ -> + dsn = options.dsn + isAttachThreads = options.attachThreads + isAttachStacktrace = options.attachStackTrace + dist = options.dist + environment = options.environment + release = options.release + isDebug = options.debug + sessionTrackingIntervalMillis = options.sessionTrackingIntervalMillis + isEnableAutoSessionTracking = options.enableAutoSessionTracking + maxAttachmentSize = options.maxAttachmentSize + maxBreadcrumbs = options.maxBreadcrumbs + setBeforeBreadcrumb { jvmBreadcrumb, _ -> options.beforeBreadcrumb?.invoke(jvmBreadcrumb.toKmpBreadcrumb())?.toJvmBreadcrumb() } + setBeforeSend { jvmSentryEvent, hint -> + options.beforeSend?.invoke(SentryEvent(jvmSentryEvent))?.let { + jvmSentryEvent.applyKmpEvent(it) + } + } } diff --git a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt index 4e4ab35c..9b2913cf 100644 --- a/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt +++ b/sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/UserExtensions.kt @@ -15,10 +15,10 @@ internal fun User.toJvmUser() = JvmUser().apply { internal fun JvmUser.toKmpUser() = User().apply { val scope = this@toKmpUser - id = scope.id.toString() - username = scope.username.toString() - email = scope.email.toString() - ipAddress = scope.ipAddress.toString() - other = scope.others?.toMutableMap()?.let { it } - unknown = scope.unknown?.toMutableMap()?.let { it } + id = scope.id + username = scope.username + email = scope.email + ipAddress = scope.ipAddress + other = scope.others?.toMutableMap() + unknown = scope.unknown?.toMutableMap() } diff --git a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryBaseEvent.kt b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryBaseEvent.kt new file mode 100644 index 00000000..0012ebc0 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryBaseEvent.kt @@ -0,0 +1,77 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.protocol.Breadcrumb +import io.sentry.kotlin.multiplatform.protocol.SentryId +import io.sentry.kotlin.multiplatform.protocol.User + +/** Base class for all Sentry events. */ +public abstract class SentryBaseEvent(public open var eventId: SentryId = SentryId.EMPTY_ID) { + /** The event release. */ + public open var release: String? = null + + /** + * The event environment. + * + * This string is freeform and not set by default. A release can be associated with more than + * one environment to separate them in the UI Think staging vs prod or similar. + */ + public open var environment: String? = null + + /** + * The event platform identifier. + * + * A string representing the platform the SDK is submitting from. This will be used by the + * Sentry interface to customize various components in the interface, but also to enter or skip + * stacktrace processing. + */ + public open var platform: String? = null + + /** Information about the user who triggered this event. */ + public open var user: User? = null + + /** Server or device name the event was generated on. */ + public open var serverName: String? = null + + /** The event distribution. Think about it together with release and environment */ + public open var dist: String? = null + + /** + * The event contexts describing the environment (e.g. device, os or browser). + * + * This is not thread-safe. + */ + public var contexts: Map = mapOf() + internal set + + /** + * A mutable map of breadcrumbs that led to this event. + * + * This is not thread-safe. + */ + public open var breadcrumbs: MutableList = mutableListOf() + + /** + * A mutable map of custom tags for this event where each tag must be less than 200 characters. + * + * This is not thread-safe. + */ + public open var tags: MutableMap = mutableMapOf() + + public fun getTag(key: String): String? = tags.get(key) + + public fun removeTag(key: String) { + tags.remove(key) + } + + public fun setTag(key: String, value: String) { + tags.set(key, value) + } + + public fun addBreadcrumb(breadcrumb: Breadcrumb) { + breadcrumbs.add(breadcrumb) + } + + public fun addBreadcrumb(message: String?) { + addBreadcrumb(Breadcrumb(message = message)) + } +} diff --git a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt new file mode 100644 index 00000000..561c3a07 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryEvent.kt @@ -0,0 +1,34 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.protocol.Message +import io.sentry.kotlin.multiplatform.protocol.SentryException + +/** Represents an event that is sent to Sentry. */ +public expect class SentryEvent() : SentryBaseEvent { + /** The event message. */ + public var message: Message? + + /** Logger that created the event. */ + public var logger: String? + + /** Severity level of the event. */ + public var level: SentryLevel? + + /** + * Manual fingerprint override. + * + * A list of strings used to dictate how this event is supposed to be grouped with other events + * into issues. For more information about overriding grouping see + * [Customize Grouping with Fingerprints](https://docs.sentry.io/data-management/event-grouping/). + * + * This is not thread-safe. + */ + public var fingerprint: MutableList + + /** + * One or multiple chained (nested) exceptions. + * + * This is not thread-safe. + */ + public var exceptions: MutableList +} 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 015cc3b7..547f45a7 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 @@ -58,6 +58,9 @@ public open class SentryOptions { /** Hook that is triggered before a breadcrumb is sent to Sentry */ public var beforeBreadcrumb: ((Breadcrumb) -> Breadcrumb?)? = null + /** Hook that is triggered before an event is sent to Sentry */ + public var beforeSend: ((SentryEvent) -> SentryEvent?)? = null + /** Information about the Sentry SDK that generated this event. */ public var sdk: SdkVersion? = null diff --git a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/Message.kt b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/Message.kt new file mode 100644 index 00000000..8c8939fa --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/Message.kt @@ -0,0 +1,7 @@ +package io.sentry.kotlin.multiplatform.protocol + +public data class Message( + public var message: String? = null, + public var params: List? = null, + public var formatted: String? = null +) diff --git a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/SentryException.kt b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/SentryException.kt new file mode 100644 index 00000000..8226a462 --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/SentryException.kt @@ -0,0 +1,8 @@ +package io.sentry.kotlin.multiplatform.protocol + +public data class SentryException( + val type: String? = null, + val value: String? = null, + val module: String? = null, + val threadId: Long? = null +) diff --git a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/User.kt b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/User.kt index 6706ef5a..e6dd939a 100644 --- a/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/User.kt +++ b/sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/protocol/User.kt @@ -2,13 +2,13 @@ package io.sentry.kotlin.multiplatform.protocol public data class User( /** The user's email */ - var email: String = "", + var email: String? = null, /** The user's id */ - var id: String = "", + var id: String? = null, /** The user's username */ - var username: String = "", + var username: String? = null, /** The user's ip address*/ var ipAddress: String? = null, @@ -34,5 +34,5 @@ public data class User( // This secondary constructor allows Swift also to init without specifying nil explicitly // example: User.init() instead of User.init(user: nil) - public constructor() : this("", "", "", null, null, null) + public constructor() : this(null, null, null, null, null, null) } diff --git a/sentry-kotlin-multiplatform/src/commonTest/kotlin/io/sentry/kotlin/multiplatform/BeforeSendTest.kt b/sentry-kotlin-multiplatform/src/commonTest/kotlin/io/sentry/kotlin/multiplatform/BeforeSendTest.kt new file mode 100644 index 00000000..922515ab --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonTest/kotlin/io/sentry/kotlin/multiplatform/BeforeSendTest.kt @@ -0,0 +1,247 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.protocol.Breadcrumb +import io.sentry.kotlin.multiplatform.protocol.Message +import io.sentry.kotlin.multiplatform.protocol.SentryException +import io.sentry.kotlin.multiplatform.protocol.User +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** Tests that verify if the beforeSend hook correctly modifies events */ +class BeforeSendTest { + + @Test + fun `beforeSend drops event`() { + val options = SentryOptions() + options.beforeSend = { + null + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(null, event) + } + + @Test + fun `beforeSend modifies message`() { + val expected = Message("test") + + val options = SentryOptions() + options.beforeSend = { + it.message = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.message) + } + + @Test + fun `beforeSend modifies logger`() { + val expected = "test" + + val options = SentryOptions() + options.beforeSend = { + it.logger = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.logger) + } + + @Test + fun `beforeSend modifies level`() { + val expected = SentryLevel.DEBUG + + val options = SentryOptions() + options.beforeSend = { + it.level = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.level) + } + + @Test + fun `beforeSend modifies fingerprint`() { + val expected = mutableListOf("test") + + val options = SentryOptions() + options.beforeSend = { + it.fingerprint = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.fingerprint) + } + + @Test + fun `beforeSend modifies release`() { + val expected = "test" + + val options = SentryOptions() + options.beforeSend = { + it.release = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.release) + } + + @Test + fun `beforeSend modifies environment`() { + val expected = "test" + + val options = SentryOptions() + options.beforeSend = { + it.environment = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.environment) + } + + @Test + fun `beforeSend modifies platform`() { + val expected = "test" + + val options = SentryOptions() + options.beforeSend = { + it.platform = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.platform) + } + + @Test + fun `beforeSend modifies user`() { + val expected = User().apply { + id = "test" + username = "username" + email = "email" + } + + val options = SentryOptions() + options.beforeSend = { + it.user = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.user) + } + + @Test + fun `beforeSend modifies serverName`() { + val expected = "test" + + val options = SentryOptions() + options.beforeSend = { + it.serverName = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.serverName) + } + + @Test + fun `beforeSend modifies dist`() { + val expected = "test" + + val options = SentryOptions() + options.beforeSend = { + it.dist = expected + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertEquals(expected, event?.dist) + } + + @Test + fun `beforeSend modifies breadcrumbs`() { + val expected = Breadcrumb.debug("test breadcrumb") + + val options = SentryOptions() + options.beforeSend = { + it.addBreadcrumb(expected) + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertTrue(event?.breadcrumbs?.contains(expected) ?: false) + } + + @Test + fun `beforeSend modifies tags`() { + val expectedKey = "key" + val expectedValue = "value" + + val options = SentryOptions() + options.beforeSend = { + it.setTag(expectedKey, expectedValue) + it + } + + val event = options.beforeSend?.invoke(SentryEvent()) + + assertTrue(event?.tags?.containsKey(expectedKey) ?: false) + assertEquals(event?.tags?.get(expectedKey), expectedValue) + } + + @Test + fun `beforeSend receives contexts`() { + var contexts: Map? = mapOf() + val options = SentryOptions() + options.beforeSend = { + contexts = it.contexts + it + } + + val event = options.beforeSend?.invoke( + SentryEvent().apply { + contexts = mapOf("test" to "test") + } + ) + + assertEquals(contexts, event?.contexts) + } + + @Test + fun `beforeSend modifies exceptions`() { + var exceptions: List? = listOf() + val options = SentryOptions() + options.beforeSend = { + exceptions = it.exceptions + it + } + + val event = options.beforeSend?.invoke( + SentryEvent().apply { + exceptions = listOf(SentryException("test")) + } + ) + + assertEquals(exceptions, event?.exceptions) + } +} diff --git a/sentry-kotlin-multiplatform/src/commonTest/kotlin/io/sentry/kotlin/multiplatform/SentryEventTest.kt b/sentry-kotlin-multiplatform/src/commonTest/kotlin/io/sentry/kotlin/multiplatform/SentryEventTest.kt new file mode 100644 index 00000000..b9e9607f --- /dev/null +++ b/sentry-kotlin-multiplatform/src/commonTest/kotlin/io/sentry/kotlin/multiplatform/SentryEventTest.kt @@ -0,0 +1,100 @@ +package io.sentry.kotlin.multiplatform + +import io.sentry.kotlin.multiplatform.protocol.Breadcrumb +import io.sentry.kotlin.multiplatform.protocol.Message +import io.sentry.kotlin.multiplatform.protocol.SentryException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SentryEventTest { + + @Test + fun `setTag should add a new tag`() { + val event = SentryEvent() + event.setTag("key", "value") + assertEquals("value", event.getTag("key")) + } + + @Test + fun `removeTag should remove an existing tag`() { + val event = SentryEvent() + event.setTag("key", "value") + event.removeTag("key") + assertNull(event.getTag("key")) + } + + @Test + fun `addBreadcrumb should add a new breadcrumb`() { + val event = SentryEvent() + val breadcrumb = Breadcrumb(message = "test") + event.addBreadcrumb(breadcrumb) + assertEquals(mutableListOf(breadcrumb), event.breadcrumbs) + } + + @Test + fun `addBreadcrumb should add a new breadcrumb with a message`() { + val event = SentryEvent() + event.addBreadcrumb("test") + assertEquals(1, event.breadcrumbs.size) + assertEquals("test", event.breadcrumbs[0].message) + } + + @Test + fun `message should be set`() { + val event = SentryEvent() + event.message = Message("test") + assertEquals("test", event.message?.message) + } + + @Test + fun `contexts should contain value if not empty`() { + val event = SentryEvent() + event.contexts = mutableMapOf("key" to "value") + assertEquals("value", event.contexts["key"]) + } + + @Test + fun `breadcrumbs should contain value if not empty`() { + val event = SentryEvent() + val breadcrumb = Breadcrumb(message = "test") + event.breadcrumbs = mutableListOf(breadcrumb) + assertEquals(mutableListOf(breadcrumb), event.breadcrumbs) + } + + @Test + fun `tags should contain value if not empty`() { + val event = SentryEvent() + event.tags = mutableMapOf("key" to "value") + assertEquals("value", event.tags["key"]) + } + + @Test + fun `fingerprint should be empty by default`() { + val event = SentryEvent() + assertTrue(event.fingerprint.isEmpty()) + } + + @Test + fun `fingerprint should contain value if not empty`() { + val event = SentryEvent() + event.fingerprint = mutableListOf("error", "exception") + assertEquals(mutableListOf("error", "exception"), event.fingerprint) + } + + @Test + fun `exceptions should be empty by default`() { + val event = SentryEvent() + assertTrue(event.exceptions.isEmpty()) + } + + @Test + fun `exceptions should contain value if not empty`() { + val event = SentryEvent() + val exception1 = SentryException(type = "NullPointerException") + val exception2 = SentryException(type = "IllegalArgumentException") + event.exceptions = mutableListOf(exception1, exception2) + assertEquals(mutableListOf(exception1, exception2), event.exceptions) + } +} diff --git a/sentry-samples/kmp-app/shared/src/commonMain/kotlin/sample.kmp.app/AppSetup.kt b/sentry-samples/kmp-app/shared/src/commonMain/kotlin/sample.kmp.app/AppSetup.kt index 24515ee2..c094d11d 100644 --- a/sentry-samples/kmp-app/shared/src/commonMain/kotlin/sample.kmp.app/AppSetup.kt +++ b/sentry-samples/kmp-app/shared/src/commonMain/kotlin/sample.kmp.app/AppSetup.kt @@ -52,5 +52,12 @@ private fun optionsConfiguration(): OptionsConfiguration { breadcrumb.message = "Add message before every breadcrumb" breadcrumb } + it.beforeSend = { event -> + if (event.environment == "test") { + null + } else { + event + } + } } }