Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(telemetry): allow to disable telemetry through settings INTELLIJ-12 #8

Merged
merged 12 commits into from
Jun 12, 2024
2 changes: 1 addition & 1 deletion .github/workflows/quality-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ jobs:
report_paths: '**/build/test-results/test/TEST-*.xml'
- name: Generate Coverage Report
run: |
./gradlew --quiet --console=plain "jacocoTestReport"
./gradlew "jacocoTestReport" $(./gradlew "jacocoTestReport" --dry-run | awk '/^:/ { print "-x" $1 }' | sed '$ d')
- uses: actions/upload-artifact@v4
name: Upload Functional Test Coverage
with:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ replay_pid*
# This file is generated at compile time.
packages/jetbrains-plugin/src/main/resources/build.properties
/**/video
/**/.DS_Store
/**/.DS_Store

FAKE_BROWSER_OUTPUT
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-12](https://jira.mongodb.org/browse/INTELLIJ-11): Notify users about telemetry, and allow them to disable it.
* [INTELLIJ-11](https://jira.mongodb.org/browse/INTELLIJ-11): Flush pending analytics events before closing the IDE.

### Changed
Expand Down
2 changes: 2 additions & 0 deletions gradle/diktat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@
- name: FILE_NAME_INCORRECT # File name does not need to match class name inside
enabled: false
- name: LOCAL_VARIABLE_EARLY_DECLARATION # Allow declaring variables at the beginning of a function if they are mutable
enabled: false
- name: USE_DATA_CLASS # Do not force to use data classes, some intellij components won't work if they are data classes
enabled: false
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
/**
* These classes implement the balloon that shows the first time that the plugin is activated.
*/

package com.mongodb.jbplugin

import com.intellij.ide.BrowserUtil
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.mongodb.jbplugin.i18n.TelemetryMessages
import com.mongodb.jbplugin.observability.probe.PluginActivatedProbe
import com.mongodb.jbplugin.settings.PluginSettingsConfigurable
import com.mongodb.jbplugin.settings.useSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
* Class that represents the link that opens the settings page for MongoDB.
*/
class OpenMongoDbPluginSettingsAction : AnAction(TelemetryMessages.message("action.disable-telemetry")) {
override fun actionPerformed(event: AnActionEvent) {
ShowSettingsUtil.getInstance().showSettingsDialog(event.project, PluginSettingsConfigurable::class.java)
}
}

/**
* Class that represents the link that opens the privacy policy page
*/
class OpenPrivacyPolicyPage : AnAction(TelemetryMessages.message("action.view-privacy-policy")) {
override fun actionPerformed(event: AnActionEvent) {
BrowserUtil.browse(TelemetryMessages.message("settings.telemetry-privacy-policy"))
}
}

/**
* This notifies that the plugin has been activated.
*
Expand All @@ -18,6 +49,23 @@ class ActivatePluginPostStartupActivity(private val cs: CoroutineScope) : Startu
cs.launch {
val pluginActivated = ApplicationManager.getApplication().getService(PluginActivatedProbe::class.java)
pluginActivated.pluginActivated()

val settings = useSettings()
if (!settings.hasTelemetryOptOutputNotificationBeenShown) {
NotificationGroupManager.getInstance()
.getNotificationGroup("com.mongodb.jbplugin.notifications.Telemetry")
.createNotification(
TelemetryMessages.message("notification.title"),
TelemetryMessages.message("notification.message"),
NotificationType.INFORMATION,
)
.setImportant(true)
.addAction(OpenPrivacyPolicyPage())
.addAction(OpenMongoDbPluginSettingsAction())
.notify(project)

settings.hasTelemetryOptOutputNotificationBeenShown = true
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mongodb.jbplugin.i18n

import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey

object SettingsMessages {
@NonNls
private const val BUNDLE = "messages.SettingsBundle"
private val instance = DynamicBundle(SettingsMessages::class.java, BUNDLE)

fun message(
key:
@PropertyKey(resourceBundle = BUNDLE)
String,
vararg params: Any,
): @Nls String = instance.getMessage(key, *params)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mongodb.jbplugin.i18n

import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey

object TelemetryMessages {
@NonNls
private const val BUNDLE = "messages.TelemetryBundle"
private val instance = DynamicBundle(TelemetryMessages::class.java, BUNDLE)

fun message(
key:
@PropertyKey(resourceBundle = BUNDLE)
String,
vararg params: Any,
): @Nls String = instance.getMessage(key, *params)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.mongodb.jbplugin.meta.BuildInformation
import com.mongodb.jbplugin.settings.useSettings
import com.segment.analytics.Analytics
import com.segment.analytics.messages.IdentifyMessage
import com.segment.analytics.messages.TrackMessage
Expand All @@ -18,48 +19,61 @@ private val logger: Logger = logger<TelemetryService>()
*/
@Service
internal class TelemetryService : AppLifecycleListener {
internal var analytics: Analytics = Analytics
.builder(BuildInformation.segmentApiKey)
.build()
internal var analytics: Analytics =
Analytics
.builder(BuildInformation.segmentApiKey)
.build()

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

fun sendEvent(event: TelemetryEvent) {
val runtimeInformationService = ApplicationManager.getApplication().getService(
RuntimeInformationService::class.java
)
val runtimeInfo = runtimeInformationService.get()
if (!useSettings().isTelemetryEnabled) {
return
}

val message = when (event) {
is TelemetryEvent.PluginActivated -> IdentifyMessage.builder().userId(runtimeInfo.userId)
else ->
TrackMessage.builder(event.name).userId(runtimeInfo.userId)
.properties(event.properties.entries.associate {
it.key.publicName to it.value
})
val runtimeInformationService =
ApplicationManager.getApplication().getService(
RuntimeInformationService::class.java,
)
val runtimeInfo = runtimeInformationService.get()

}
val message =
when (event) {
is TelemetryEvent.PluginActivated -> IdentifyMessage.builder().userId(runtimeInfo.userId)
else ->
TrackMessage.builder(event.name).userId(runtimeInfo.userId)
.properties(
event.properties.entries.associate {
it.key.publicName to it.value
},
)
}

analytics.enqueue(message)
}

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

analytics.flush()
if (telemetryEnabled) {
analytics.flush()
}

analytics.shutdown()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Settings state for the plugin. These classes are responsible for the persistence of the plugin
* settings.
addaleax marked this conversation as resolved.
Show resolved Hide resolved
*/

package com.mongodb.jbplugin.settings

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
import java.io.Serializable

/**
* The state component represents the persisting state. Don't use directly, this is only necessary
* for the state to be persisted. Use PluginSettings instead.
*
* @see PluginSettings
*/
@Service
@State(
name = "com.mongodb.jbplugin.settings.PluginSettings",
storages = [Storage(value = "MongoDBPluginSettings.xml")],
)
class PluginSettingsStateComponent : SimplePersistentStateComponent<PluginSettings>(PluginSettings())

/**
* The settings themselves. They are tracked, so any change on the settings properties will be eventually
* persisted by IntelliJ. To access the settings, use the useSettings provider.
*
* @see useSettings
*/
class PluginSettings : BaseState(), Serializable {
var isTelemetryEnabled by property(true)
var hasTelemetryOptOutputNotificationBeenShown by property(false)
}

/**
* Function that provides a reference to the current PluginSettings.
*
* @see PluginSettings
*
* @return
*/
fun useSettings(): PluginSettings =
ApplicationManager.getApplication().getService(
PluginSettingsStateComponent::class.java,
).state
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* These classes represent the settings modal.
*/

package com.mongodb.jbplugin.settings

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.options.Configurable
import com.intellij.ui.components.JBCheckBox
import com.intellij.util.ui.FormBuilder
import com.mongodb.jbplugin.i18n.SettingsMessages
import com.mongodb.jbplugin.i18n.TelemetryMessages
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel

/**
* This class represents a section in the settings modal. The UI will be implemented by
* PluginSettingsComponent.
*/
class PluginSettingsConfigurable : Configurable {
private lateinit var settingsComponent: PluginSettingsComponent

override fun createComponent(): JComponent {
settingsComponent = PluginSettingsComponent()
return settingsComponent.root
}

override fun isModified(): Boolean {
val savedSettings = useSettings()
return settingsComponent.isTelemetryEnabledCheckBox.isSelected != savedSettings.isTelemetryEnabled
}

override fun apply() {
val savedSettings =
useSettings().apply {
isTelemetryEnabled = settingsComponent.isTelemetryEnabledCheckBox.isSelected
}
}

override fun reset() {
val savedSettings = useSettings()
settingsComponent.isTelemetryEnabledCheckBox.isSelected = savedSettings.isTelemetryEnabled
}

override fun getDisplayName() = SettingsMessages.message("settings.display-name")
}

/**
* The panel that is shown in the settings section for MongoDB.
*/
private class PluginSettingsComponent {
val root: JPanel
val isTelemetryEnabledCheckBox = JBCheckBox(TelemetryMessages.message("settings.telemetry-collection-checkbox"))
val privacyPolicyButton = JButton(TelemetryMessages.message("action.view-privacy-policy"))

init {
privacyPolicyButton.addActionListener {
BrowserUtil.browse(TelemetryMessages.message("settings.telemetry-privacy-policy"))
}

root =
FormBuilder.createFormBuilder()
.addComponent(isTelemetryEnabledCheckBox)
.addTooltip(TelemetryMessages.message("settings.telemetry-collection-tooltip"))
.addComponent(privacyPolicyButton)
.addComponentFillVertically(JPanel(), 0)
.panel

root.accessibleContext.accessibleName = "MongoDB Settings"
isTelemetryEnabledCheckBox.accessibleContext.accessibleName = "MongoDB Enable Telemetry"
}
}
10 changes: 10 additions & 0 deletions packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@

<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="com.mongodb.jbplugin.ActivatePluginPostStartupActivity"/>
<applicationConfigurable
parentId="tools"
instance="com.mongodb.jbplugin.settings.PluginSettingsConfigurable"
id="com.mongodb.jbplugin.settings.PluginSettingsConfigurable"
displayName="MongoDB"/>
<notificationGroup id="com.mongodb.jbplugin.notifications.Telemetry"
displayType="BALLOON"
bundle="messages.TelemetryBundle"
key="notification.group.name"
/>
</extensions>
<applicationListeners>
</applicationListeners>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
settings.display-name=MongoDB
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
notification.group.name=MongoDB telemetry
notification.title=MongoDB plugin telemetry
notification.message=Anonymous telemetry is enabled by default because it helps us improve the plugin.
action.disable-telemetry=Disable Telemetry
action.view-privacy-policy=View Privacy Policy
settings.telemetry-collection-tooltip=Allow the collection of anonymous diagnostics and usage telemetry data to help improve the product.
settings.telemetry-collection-checkbox=Enable telemetry
settings.telemetry-privacy-policy=https://www.mongodb.com/legal/privacy/privacy-policy
Loading
Loading