Skip to content

Commit

Permalink
chore: flush telemetry when IDE closes INTELLIJ-11 (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmruiz authored Jun 11, 2024
1 parent d531866 commit 0cb18c0
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 148 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ MongoDB plugin for IntelliJ IDEA.
## [Unreleased]

### Added
* [INTELLIJ-11](https://jira.mongodb.org/browse/INTELLIJ-11): Flush pending analytics events before closing the IDE.

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mongodb.jbplugin

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
Expand All @@ -15,7 +16,7 @@ import kotlinx.coroutines.launch
class ActivatePluginPostStartupActivity(private val cs: CoroutineScope) : StartupActivity, DumbAware {
override fun runActivity(project: Project) {
cs.launch {
val pluginActivated = project.getService(PluginActivatedProbe::class.java)
val pluginActivated = ApplicationManager.getApplication().getService(PluginActivatedProbe::class.java)
pluginActivated.pluginActivated()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ package com.mongodb.jbplugin.observability

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project

/**
* @param gson
Expand All @@ -33,25 +33,26 @@ internal class LogMessageBuilder(private val gson: Gson, message: String) {
/**
* This class will be injected in probes to build log messages. Usually like:
* ```kt
* @Service(Service.Level.PROJECT)
* class MyProbe(private val project: Project) {
* @Service
* class MyProbe {
* ...
* fun somethingProbed() {
* val logMessage = project.getService(LogMessage::class.java)
* val logMessage = ApplicationManager.getApplication().getService(LogMessage::class.java)
* log.info(logMessage.message("My message").put("someOtherProp", 25).build())
* }
* ...
* }
* ```
*
* @param project
*/
@Service(Service.Level.PROJECT)
internal class LogMessage(private val project: Project) {
@Service
internal class LogMessage {
private val gson = GsonBuilder().generateNonExecutableJson().disableJdkUnsafe().create()

fun message(key: String): LogMessageBuilder {
val runtimeInformationService = project.getService(RuntimeInformationService::class.java)
val runtimeInformationService = ApplicationManager.getApplication().getService(
RuntimeInformationService::class.java
)
val runtimeInformation = runtimeInformationService.get()

return LogMessageBuilder(gson, key)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
package com.mongodb.jbplugin.observability

import com.intellij.ide.AppLifecycleListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.mongodb.jbplugin.meta.BuildInformation
import com.segment.analytics.Analytics
import com.segment.analytics.messages.IdentifyMessage
import com.segment.analytics.messages.TrackMessage

private val logger: Logger = logger<TelemetryService>()

/**
* This telemetry service is used to send events to Segment. Should be used within
* probes, no directly. That is why it's marked as internal.
*
* @param project
*/
@Service(Service.Level.PROJECT)
internal class TelemetryService(private val project: Project) {
@Service
internal class TelemetryService : AppLifecycleListener {
internal var analytics: Analytics = Analytics
.builder(BuildInformation.segmentApiKey)
.build()

init {
ApplicationManager.getApplication()
.messageBus
.connect()
.subscribe(
AppLifecycleListener.TOPIC,
this
)
}

fun sendEvent(event: TelemetryEvent) {
val runtimeInformationService = project.getService(RuntimeInformationService::class.java)
val runtimeInformationService = ApplicationManager.getApplication().getService(
RuntimeInformationService::class.java
)
val runtimeInfo = runtimeInformationService.get()

val message = when (event) {
Expand All @@ -35,4 +50,16 @@ internal class TelemetryService(private val project: Project) {

analytics.enqueue(message)
}

override fun appWillBeClosed(isRestart: Boolean) {
val logMessage = ApplicationManager.getApplication().getService(LogMessage::class.java)
logger.info(
logMessage.message("Flushing Segment analytics because the IDE is closing.")
.put("isRestart", isRestart)
.build()
)

analytics.flush()
analytics.shutdown()
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.mongodb.jbplugin.observability.probe

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.mongodb.jbplugin.observability.LogMessage
import com.mongodb.jbplugin.observability.TelemetryEvent
import com.mongodb.jbplugin.observability.TelemetryService
Expand All @@ -12,14 +12,13 @@ private val logger: Logger = logger<PluginActivatedProbe>()

/**
* This probe is emitted when the plugin is activated (started).
*
* @param project Project where the plugin is set up
*/
@Service(Service.Level.PROJECT)
class PluginActivatedProbe(private val project: Project) {
@Service
class PluginActivatedProbe {
fun pluginActivated() {
val telemetry = project.getService(TelemetryService::class.java)
val logMessage = project.getService(LogMessage::class.java)
val application = ApplicationManager.getApplication()
val telemetry = application.getService(TelemetryService::class.java)
val logMessage = application.getService(LogMessage::class.java)

telemetry.sendEvent(TelemetryEvent.PluginActivated)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.mongodb.jbplugin

import com.intellij.openapi.application.Application
import com.intellij.openapi.project.Project
import com.mongodb.jbplugin.fixtures.IntegrationTest
import com.mongodb.jbplugin.fixtures.eventually
import com.mongodb.jbplugin.fixtures.mockProject
import com.mongodb.jbplugin.fixtures.withMockedService
import com.mongodb.jbplugin.observability.probe.PluginActivatedProbe
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
Expand All @@ -11,11 +14,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking

@IntegrationTest
class ActivatePluginPostStartupActivityTest {
@Test
fun `emits a plugin activated probe`() = runBlocking {
fun `emits a plugin activated probe`(application: Application, project: Project) = runBlocking {
val pluginActivatedProbe = mock<PluginActivatedProbe>()
val project = mockProject(pluginActivatedProbe = pluginActivatedProbe)
application.withMockedService(pluginActivatedProbe)

val listener = ActivatePluginPostStartupActivity(CoroutineScope(Dispatchers.Default))

listener.runActivity(project)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Extension for tests that depend on an Application.
*/

package com.mongodb.jbplugin.fixtures

import com.google.gson.Gson
import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.project.Project
import com.intellij.util.messages.ListenerDescriptor
import com.intellij.util.messages.MessageBus
import com.intellij.util.messages.MessageBusOwner
import com.intellij.util.messages.impl.RootBus
import com.mongodb.jbplugin.observability.LogMessage
import com.mongodb.jbplugin.observability.LogMessageBuilder
import com.mongodb.jbplugin.observability.RuntimeInformation
import com.mongodb.jbplugin.observability.RuntimeInformationService
import org.junit.jupiter.api.extension.*
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.kotlin.any

/**
* Annotation to be used within the test. It provides, as a parameter of a test,
* either a mocked setup Application or Project.
*
* @see com.mongodb.jbplugin.observability.LogMessageTest
*/
@ExtendWith(IntegrationTestExtension::class)
annotation class IntegrationTest

/**
* Extension class, should not be used directly.
*/
private class IntegrationTestExtension : BeforeTestExecutionCallback,
AfterTestExecutionCallback,
ParameterResolver {
private lateinit var application: Application
private lateinit var messageBus: MessageBus
private lateinit var project: Project

override fun beforeTestExecution(context: ExtensionContext?) {
application = mock()
project = mock()

messageBus = RootBus(mock<MessageBusOwner>().apply {
`when`(this.isDisposed()).thenReturn(false)
`when`(this.isParentLazyListenersIgnored()).thenReturn(false)
`when`(this.createListener(any())).then {
val descriptor = it.arguments[0] as ListenerDescriptor
Class.forName(descriptor.listenerClassName).getConstructor().newInstance()
}
})

`when`(application.getMessageBus()).thenReturn(messageBus)
`when`(project.getMessageBus()).thenReturn(messageBus)

ApplicationManager.setApplication(application) {}
}

override fun afterTestExecution(context: ExtensionContext?) {
}

override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean =
parameterContext?.parameter?.type?.run {
equals(Application::class.java) || equals(Project::class.java)
} ?: false

override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any =
when (parameterContext?.parameter?.type) {
Application::class.java -> application
Project::class.java -> project
else -> TODO()
}
}

/**
* Convenience function in application or project for tests. It mocks the implementation of a single service
* with whatever implementation is passed as a parameter. For example:
*
* ```kt
* application.withMockedService(mockRuntimeInformationService())
* project.withMockedService(mockRuntimeInformationService())
* ```
*
* @param serviceImpl
* @return itself so it can be chained
*/
inline fun <reified S : ComponentManager, reified T> S.withMockedService(serviceImpl: T): S {
`when`(getService(T::class.java)).thenReturn(serviceImpl)
return this
}

/**
* Generates a mock runtime information service, useful for testing. If you need
* to create your own. You'll likely will build first an information service and
* then inject it into a mock project, something like this:
*
* ```kt
* val myInfoService = mockRuntimeInformationService(userId = "hey")
* project.withMockedService(myInfoService)
* ```
*
* @param userId
* @param osName
* @param arch
* @param jvmVendor
* @param jvmVersion
* @param buildVersion
* @param applicationName
* @return A new mocked RuntimeInformationService
*/
internal fun mockRuntimeInformationService(
userId: String = "123456",
osName: String = "Winux OSX",
arch: String = "x128",
jvmVendor: String = "Obelisk",
jvmVersion: String = "42",
buildVersion: String = "2024.2",
applicationName: String = "Cool IDE"
) = mock<RuntimeInformationService>().also { service ->
`when`(service.get()).thenReturn(
RuntimeInformation(
userId = userId,
osName = osName,
arch = arch,
jvmVendor = jvmVendor,
jvmVersion = jvmVersion,
buildVersion = buildVersion,
applicationName = applicationName
)
)
}

/**
* Generates a mock log message service.
* You'll likely will build first a log message service and
* then inject it into a mock project, something like this:
*
* ```kt
* val myLogMessage = mockLogMessage()
* project.withMockedService(myLogMessage)
* ```
*
* @return A new mocked LogMessage
*/
internal fun mockLogMessage() = mock<LogMessage>().also { logMessage ->
`when`(logMessage.message(any())).then { message ->
LogMessageBuilder(Gson(), message.arguments[0].toString())
}
}
Loading

0 comments on commit 0cb18c0

Please sign in to comment.