Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import kotlin.concurrent.thread

/**
* An app running in a child JVM, for apps that must not share the test's JVM, such as one with the
* OpenTelemetry agent attached.
* OpenTelemetry agent attached or one whose exit is under test.
*
* The app writes the port it listens on to the file named by the `PORTFILE` environment variable,
* which Spring Boot's `WebServerPortFileWriter` reads too. The app's output goes to the test's
* standard output as it arrives, so Gradle's test report shows it with the test that was running.
* A server app writes the port it listens on to the file named by the `PORTFILE` environment
* variable, which Spring Boot's `WebServerPortFileWriter` reads too. The app's output goes to the
* test's standard output as it arrives, so Gradle's test report shows it with the test that was
* running.
*/
class ChildJvmApp private constructor(private val process: Process, private val workDir: File) :
Closeable {
Expand All @@ -37,6 +38,14 @@ class ChildJvmApp private constructor(private val process: Process, private val
}
}

/** Waits for the app to exit on its own and returns its exit code. */
fun awaitExit(timeoutSeconds: Long = 30): Int {
check(process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) {
"App did not exit within ${timeoutSeconds}s"
}
return process.exitValue()
}

override fun close() {
process.destroyForcibly().waitFor()
workDir.deleteRecursively()
Expand Down
113 changes: 113 additions & 0 deletions sentry/src/test/java/io/sentry/SentryEndToEndTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.sentry

import com.google.common.truth.Truth.assertThat
import io.sentry.protocol.Message
import io.sentry.test.MockSentryServer
import io.sentry.test.event
import io.sentry.test.hasFlag
import io.sentry.test.hasMetric
import io.sentry.test.hasMetricWithAttribute
import io.sentry.test.initForTest
import io.sentry.test.metrics
import io.sentry.test.transaction
import kotlin.test.AfterTest
import kotlin.test.Test

/** Reports through the APIs an app uses without integrations, over the real HTTP transport. */
class SentryEndToEndTest {
private val server = MockSentryServer()

@AfterTest
fun teardown() {
Sentry.close()
server.close()
}

@Test
fun `events, a transaction and metrics reach Sentry`() {
initForTest {
it.dsn = server.dsn
it.tracesSampleRate = 1.0
it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ ->
if (breadcrumb.message == "bad breadcrumb") null else breadcrumb
}
}
Sentry.addBreadcrumb("bad breadcrumb")
Sentry.configureScope {
it.addEventProcessor(
object : EventProcessor {
override fun process(event: SentryEvent, hint: Hint): SentryEvent = event.apply {
if ((level?.ordinal ?: 0) > SentryLevel.INFO.ordinal) {
addBreadcrumb(Breadcrumb("Processed by the event processor"))
}
}
}
)
}

Sentry.withScope {
it.level = SentryLevel.FATAL
Sentry.captureMessage("Fatal message!")
}
Sentry.captureMessage("Some warning!", SentryLevel.WARNING)
Sentry.addFeatureFlag("my-feature-flag", true)
Sentry.setAttribute("user.type", "admin")
Sentry.setAttribute("feature.version", 2)
Sentry.metrics().count("countMetric")
Sentry.metrics().gauge("gaugeMetric", 5.0)
Sentry.metrics().distribution("distributionMetric", 7.0)
Sentry.captureException(RuntimeException("Some error!"))
Sentry.captureEvent(
SentryEvent().apply {
message = Message().apply { message = "Detailed event" }
fingerprints = listOf("NewClientDebug")
level = SentryLevel.DEBUG
}
)
repeat(10) { i ->
Sentry.captureEvent(
SentryEvent().apply { message = Message().apply { message = "event $i" } }
)
}
val transaction = Sentry.startTransaction("transaction name", "op")
transaction.startChild("child").finish()
transaction.finish()
Sentry.flush(5000)

val fatal =
server.awaitEnvelope { it.event()?.message?.formatted == "Fatal message!" }.event()!!
assertThat(fatal.level).isEqualTo(SentryLevel.FATAL)
assertThat(fatal.breadcrumbs!!.map { it.message }).contains("Processed by the event processor")
assertThat(fatal.breadcrumbs!!.map { it.message }).doesNotContain("bad breadcrumb")

val warning =
server.awaitEnvelope { it.event()?.message?.formatted == "Some warning!" }.event()!!
assertThat(warning.level).isEqualTo(SentryLevel.WARNING)

val error =
server
.awaitEnvelope { it.event()?.exceptions?.any { e -> e.value == "Some error!" } == true }
.event()!!
assertThat(error.exceptions!!.map { it.type to it.value })
.contains("RuntimeException" to "Some error!")
assertThat(error.hasFlag("my-feature-flag", true)).isTrue()

val detailed =
server.awaitEnvelope { it.event()?.message?.message == "Detailed event" }.event()!!
assertThat(detailed.fingerprints).containsExactly("NewClientDebug")
assertThat(detailed.level).isEqualTo(SentryLevel.DEBUG)

repeat(10) { i -> server.awaitEnvelope { it.event()?.message?.message == "event $i" } }

val sent =
server.awaitEnvelope { it.transaction()?.transaction == "transaction name" }.transaction()!!
assertThat(sent.spans.map { it.op }).contains("child")

val metrics = server.awaitEnvelope { it.metrics() != null }.metrics()!!
assertThat(metrics.hasMetric("countMetric", "counter", 1.0)).isTrue()
assertThat(metrics.hasMetric("gaugeMetric", "gauge", 5.0)).isTrue()
assertThat(metrics.hasMetric("distributionMetric", "distribution", 7.0)).isTrue()
assertThat(metrics.hasMetricWithAttribute("countMetric", "user.type", "admin")).isTrue()
assertThat(metrics.hasMetricWithAttribute("countMetric", "feature.version", 2)).isTrue()
}
}
56 changes: 56 additions & 0 deletions sentry/src/test/java/io/sentry/ShutdownHookEndToEndTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.sentry

import com.google.common.truth.Truth.assertThat
import io.sentry.test.ChildJvmApp
import io.sentry.test.MockSentryServer
import io.sentry.test.event
import io.sentry.test.hasLogWithBody
import io.sentry.test.hasMetric
import io.sentry.test.logs
import io.sentry.test.metrics
import kotlin.test.AfterTest
import kotlin.test.Test

/**
* Runs an app in its own JVM that returns from `main` without flushing or closing Sentry, as most
* apps do, so only the shutdown hook can deliver what it reported.
*/
class ShutdownHookEndToEndTest {
private val server = MockSentryServer()

@AfterTest
fun teardown() {
server.close()
}

@Test
fun `the shutdown hook delivers what an app reported before it exited`() {
ChildJvmApp.start(
mainClass = ExitWithoutFlushApp::class.java.name,
classpath = System.getProperty("java.class.path"),
args = listOf(server.dsn),
)
.use { app ->
// A timeout here means a thread the SDK started kept the JVM from exiting.
assertThat(app.awaitExit()).isEqualTo(0)
}

server.awaitEnvelope { it.event()?.message?.formatted == "Reported before exit" }
server.awaitEnvelope { it.logs()?.hasLogWithBody("Logged before exit") == true }
server.awaitEnvelope { it.metrics()?.hasMetric("exitMetric", "counter", 1.0) == true }
}
}

/** Reports an event, a log and a metric, then returns without flushing or closing Sentry. */
object ExitWithoutFlushApp {
@JvmStatic
fun main(args: Array<String>) {
Sentry.init {
it.dsn = args[0]
it.logs.isEnabled = true
}
Sentry.captureMessage("Reported before exit")
Sentry.logger().info("Logged before exit")
Sentry.metrics().count("exitMetric")
}
}
Loading