[kotlin] Add K2 feedback survey

KTIJ-28299 ^Fixed


Co-authored-by: Ilya Kirillov <ilya.kirillov@jetbrains.com>


Merge-request: IJ-MR-123182
Merged-by: Victoria Petrakovich <Victoria.Petrakovich@jetbrains.com>

GitOrigin-RevId: db3bb8228131f749b500e6ee36db0597ff19ef35
This commit is contained in:
Victoria.Petrakovich
2024-01-08 22:49:35 +00:00
committed by intellij-monorepo-bot
co-authored by Ilya Kirillov
parent e95af7e141
commit 3c88968747
13 changed files with 419 additions and 2 deletions
@@ -0,0 +1,41 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.impl
import com.intellij.internal.statistic.utils.getPluginInfoByDescriptor
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.feedback.FeedbackSurvey
import com.intellij.platform.feedback.impl.state.DontShowAgainFeedbackService
import kotlin.reflect.KClass
@Service(Service.Level.APP)
class OnDemandFeedbackResolver {
companion object {
@JvmStatic
fun getInstance(): OnDemandFeedbackResolver = service()
private fun <S : FeedbackSurvey> getJbOnDemandFeedbackSurveyExtension(surveyClass: KClass<S>): S {
return IdleFeedbackResolver.getJbIdleFeedbackSurveyExtensionList().filterIsInstance(surveyClass.java).first()
}
private fun canShowFeedbackNotification(): Boolean {
return DontShowAgainFeedbackService.checkIsAllowedToShowFeedback() && Registry.`is`("platform.feedback", true)
}
}
/**
* Shows [survey] if it's suitable and user allows showing surveys.
*
* @return `true` if a survey notification was shown, `false` otherwise
*/
fun <S : FeedbackSurvey> showFeedbackNotification(surveyClass: KClass<S>, project: Project): Boolean {
val survey = getJbOnDemandFeedbackSurveyExtension(surveyClass)
if (!canShowFeedbackNotification()) return false
if (!survey.isSuitableToShow(project)) return false
survey.showNotification(project)
return true
}
}
@@ -9,6 +9,7 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="kotlin.plugin.k1" scope="TEST" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="kotlinc.kotlin-compiler-common" level="project" />
<orderEntry type="library" name="kotlinc.kotlin-compiler-fe10" level="project" />
@@ -18,10 +19,13 @@
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
<orderEntry type="module" module-name="intellij.platform.feedback" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="kotlin.base.plugin" />
<orderEntry type="library" name="kotlinx-datetime-jvm" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-test" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-test-assertions-core-jvm" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-test-junit" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="module" module-name="intellij.java.testFramework" scope="TEST" />
<orderEntry type="module" module-name="kotlin.preferences" />
</component>
</module>
@@ -6,7 +6,10 @@
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="org.jetbrains.kotlin.onboarding.KotlinNewUserTracker"/>
<applicationService serviceImplementation="org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.K2UserTracker"/>
<feedback.idleFeedbackSurvey implementation="org.jetbrains.kotlin.onboarding.OnboardingFeedbackSurvey"/>
<feedback.idleFeedbackSurvey implementation="org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.K2FeedbackSurvey"/>
<postStartupActivity implementation="org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.ShowK2SurveyNotificationAfterRestartPostStartupActivity"/>
</extensions>
<actions resource-bundle="messages.FeedbackBundle">
<group id="OnboardingKotlin" internal="true">
@@ -14,6 +17,24 @@
class="org.jetbrains.kotlin.onboarding.ShowKotlinOnboardingFeedbackDialogAction" internal="true"/>
<add-to-group group-id="FeedbackDialogGroup" anchor="last"/>
</group>
<group id="K2Satisfaction" internal="true">
<action id="org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.ShowK2FeedbackDialogAction"
class="org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.ShowK2FeedbackDialogAction" internal="true"/>
<add-to-group group-id="FeedbackDialogGroup" anchor="last"/>
</group>
</actions>
<extensions defaultExtensionNs="com.intellij">
<registryKey
key="test.k2.feedback.survey"
description="Enables the testing mode for K2 feedback survey"
defaultValue="false"
restartRequired="false"/>
<registryKey
key="minimum.usage.time.before.showing.k2.survey"
description="In how many seconds after the start of K2 usage to show the survey"
defaultValue="86400"
restartRequired="false"/>
</extensions>
</idea-plugin>
@@ -30,4 +30,21 @@ notification.thanks.feedback.content=You are helping improve the Kotlin plugin!
notification.request.title=Share feedback about the Kotlin plugin
notification.request.content=Please answer a few questions. It will take about 2 minutes.
action.org.jetbrains.kotlin.onboarding.ShowKotlinOnboardingFeedbackDialogAction.text=Show Kotlin Onboarding Feedback Dialog
action.org.jetbrains.kotlin.onboarding.ShowKotlinOnboardingFeedbackDialogAction.text=Show Kotlin Onboarding Feedback Dialog
dialog.k2.satisfaction.top.title=Feedback
dialog.k2.satisfaction.title=K2 Kotlin Mode Feedback
dialog.k2.satisfaction.description=You have been using K2 Kotlin mode recently, share your feedback with us.
dialog.k2.satisfaction.performance.rating.label=How satisfied are you with the IDE performance in K2-based mode?
dialog.k2.satisfaction.quality.rating.label=How satisfied are you with the quality of Kotlin support in IDE?
dialog.k2.satisfaction.biggest.quality.problems.text.label=What are the biggest quality problems you have noticed, if any?
dialog.k2.satisfaction.features.missed.most.text.label=What features did you miss the most, if any?
notification.k2.satisfaction.thanks.feedback.content=You are helping improve the Kotlin K2-based plugin!
notification.k2.satisfaction.request.title=Share feedback about the K2 Kotlin mode
notification.k2.satisfaction.request.content=Please answer a few questions. It will take about 2 minutes.
action.org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.ShowK2FeedbackDialogAction.text=Show K2 Kotlin Feedback Dialog
@@ -0,0 +1,69 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.onboarding.k2.satisfaction.survey
import com.intellij.openapi.project.Project
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialogWithEmail
import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData
import com.intellij.platform.feedback.dialog.showFeedbackSystemInfoDialog
import com.intellij.platform.feedback.dialog.uiBlocks.*
import com.intellij.platform.feedback.impl.notification.ThanksForFeedbackNotification
import org.jetbrains.kotlin.onboarding.FeedbackBundle
class K2FeedbackDialog(
project: Project?,
forTest: Boolean
) : BlockBasedFeedbackDialogWithEmail<CommonFeedbackSystemData>(project, forTest) {
/** Increase the additional number when feedback format is changed */
override val myFeedbackJsonVersion: Int = super.myFeedbackJsonVersion + 1
override val zendeskTicketTitle: String = "K2 in-IDE Feedback"
override val zendeskFeedbackType: String = "K2 in-IDE Feedback"
override val myFeedbackReportId: String = "k2_feedback"
override fun shouldAutoCloseZendeskTicket(): Boolean {
return false
}
override val mySystemInfoData: CommonFeedbackSystemData by lazy {
CommonFeedbackSystemData.getCurrentData()
}
override val myShowFeedbackSystemInfoDialog: () -> Unit = {
showFeedbackSystemInfoDialog(myProject, mySystemInfoData)
}
override val myTitle: String = FeedbackBundle.message("dialog.k2.satisfaction.top.title")
override val myBlocks: List<FeedbackBlock> = listOf(
TopLabelBlock(FeedbackBundle.message("dialog.k2.satisfaction.title")),
DescriptionBlock(FeedbackBundle.message("dialog.k2.satisfaction.description")),
RatingBlock(
FeedbackBundle.message("dialog.k2.satisfaction.performance.rating.label"),
"performance_rating"
),
RatingBlock(
FeedbackBundle.message("dialog.k2.satisfaction.quality.rating.label"),
"quality_rating"
),
TextAreaBlock(
FeedbackBundle.message("dialog.k2.satisfaction.biggest.quality.problems.text.label"),
"biggest_quality_problems"
),
TextAreaBlock(
FeedbackBundle.message("dialog.k2.satisfaction.features.missed.most.text.label"),
"most_missed_features"
)
)
init {
init()
}
override fun showThanksNotification() {
ThanksForFeedbackNotification(
description = FeedbackBundle.message(
"notification.k2.satisfaction.thanks.feedback.content"
)
).notify(myProject)
}
}
@@ -0,0 +1,14 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.onboarding.k2.satisfaction.survey
import com.intellij.platform.feedback.FeedbackSurvey
import com.intellij.platform.feedback.FeedbackSurveyType
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
import com.intellij.platform.feedback.InIdeFeedbackSurveyType
class K2FeedbackSurvey : FeedbackSurvey() {
override val feedbackSurveyType: FeedbackSurveyType<InIdeFeedbackSurveyConfig> =
InIdeFeedbackSurveyType(K2FeedbackSurveyConfig())
}
@@ -0,0 +1,50 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.onboarding.k2.satisfaction.survey
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.project.Project
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialogWithEmail
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification
import com.intellij.util.PlatformUtils
import kotlinx.datetime.LocalDate
import org.jetbrains.kotlin.onboarding.FeedbackBundle
class K2FeedbackSurveyConfig : InIdeFeedbackSurveyConfig {
override val surveyId: String = "k2_feedback"
override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2024, 3, 18)
override val requireIdeEAP: Boolean = true
private val suitableIdeVersion = "2024.1"
override fun checkIdeIsSuitable(): Boolean {
return PlatformUtils.isIdeaUltimate() || PlatformUtils.isIdeaCommunity()
}
override fun checkExtraConditionSatisfied(project: Project): Boolean {
return suitableIdeVersion == ApplicationInfo.getInstance().shortVersion &&
K2UserTracker.getInstance().shouldShowK2FeedbackDialog()
}
override fun updateStateAfterDialogClosedOk(project: Project) {
K2UserTracker.getInstance().state.userSawSurvey = true
}
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialogWithEmail<out SystemDataJsonSerializable> {
return K2FeedbackDialog(project, forTest)
}
override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification {
return RequestFeedbackNotification(
"Feedback In IDE",
FeedbackBundle.message("notification.k2.satisfaction.request.title"),
FeedbackBundle.message("notification.k2.satisfaction.request.content")
)
}
override fun updateStateAfterNotificationShowed(project: Project) {
// do nothing
}
}
@@ -0,0 +1,86 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.onboarding.k2.satisfaction.survey
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.registry.Registry
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginModeProvider
import org.jetbrains.kotlin.idea.configuration.KotlinPluginKindSwitcherListener
import java.time.Duration
import java.time.Instant
class K2UserTrackerState : BaseState(), KotlinPluginKindSwitcherListener {
// Unix time seconds
var k2UserSince by property(Instant.now().epochSecond) // We don't have any information earlier, so always start from now
/* We need to store that the user saw the survey because the state in com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService
doesn't migrate when updating the IDE */
var userSawSurvey by property(false)
// The following flag is needed to explicitly define that the user has just switched to K1 – we need this state after IDE restart
var switchedToK1 by property(false) // If they are on K1 from the beginning, then they didn't switch
override fun kotlinPluginKindChanged(kotlinPluginMode: KotlinPluginMode) {
if (kotlinPluginMode == KotlinPluginMode.K2) {
switchedToK1 = false
k2UserSince = Instant.now().epochSecond
} else if (kotlinPluginMode == KotlinPluginMode.K1) {
switchedToK1 = true
}
}
}
@State(name = "K2NewUserTracker", storages = [Storage("k2-feedback.xml")])
class K2UserTracker : PersistentStateComponent<K2UserTrackerState> {
companion object {
private val LOG = Logger.getInstance(K2UserTracker::class.java)
fun getInstance(): K2UserTracker {
return service()
}
}
internal var forUnitTests = false
internal var k2PluginModeForTests = false // Want the test to not depend on a real K1/K2 mode
internal var currentState = K2UserTrackerState()
override fun getState(): K2UserTrackerState = currentState
override fun loadState(state: K2UserTrackerState) {
currentState = state
}
internal fun shouldShowK2FeedbackDialog(): Boolean {
if (!Registry.`is`("test.k2.feedback.survey", false)) {
if (!forUnitTests) {
if (ApplicationManager.getApplication().isInternal) return false // Don't show in Nightly builds or in `IDEA (dev build)`
}
if (state.userSawSurvey) return false // We show this survey only once
} else {
state.userSawSurvey = false // We reset this state for manual testing to be able to see the survey more than once
}
if (state.switchedToK1) {
return true
} else {
val k2Chosen = if (forUnitTests) {
k2PluginModeForTests
} else {
KotlinPluginModeProvider.currentPluginMode == KotlinPluginMode.K2
}
if (!k2Chosen) {
LOG.debug("Not showing the K2 feedback dialog because the user doesn't use K2")
return false
} else {
val k2UserSince = Instant.ofEpochSecond(state.k2UserSince)
val durationSinceK2User = Duration.between(k2UserSince, Instant.now())
LOG.debug("Duration since user became a K2 Kotlin user: ${durationSinceK2User.toDays()} day(s)")
return durationSinceK2User > Duration.ofSeconds(
Registry.intValue("minimum.usage.time.before.showing.k2.survey").toLong()
)
}
}
}
}
@@ -0,0 +1,11 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.onboarding.k2.satisfaction.survey
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
class ShowK2FeedbackDialogAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
K2FeedbackDialog(e.project, true).show()
}
}
@@ -0,0 +1,21 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.onboarding.k2.satisfaction.survey
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.platform.feedback.impl.OnDemandFeedbackResolver
internal class ShowK2SurveyNotificationAfterRestartPostStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
val propertiesComponent = serviceAsync<PropertiesComponent>()
val propertiesComponentKey = "K2KotlinSurveyWasProposedOnAppRestart"
if (!propertiesComponent.getBoolean(propertiesComponentKey)) {
if (OnDemandFeedbackResolver.getInstance().showFeedbackNotification(K2FeedbackSurvey::class, project)) {
propertiesComponent.updateValue(propertiesComponentKey, /* newValue = */ true)
}
}
}
}
@@ -0,0 +1,66 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package onboarding.k2.satisfaction.survey
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import org.jetbrains.kotlin.onboarding.k2.satisfaction.survey.K2UserTracker
import java.time.Instant
class K2UserTrackerTest: LightJavaCodeInsightFixtureTestCase() {
private fun createInstance(): K2UserTracker {
val tracker = K2UserTracker()
tracker.forUnitTests = true
tracker.k2PluginModeForTests = true
return tracker
}
fun `test the empty state should have correct values`() {
val instance = createInstance()
assertFalse(instance.state.switchedToK1)
assertFalse(instance.state.userSawSurvey)
}
fun `test don't show dialog without explicit switch to K1`() {
val instance = createInstance()
assertFalse(instance.state.switchedToK1)
assertFalse(instance.state.userSawSurvey)
assertFalse(instance.shouldShowK2FeedbackDialog())
assertFalse(instance.state.userSawSurvey)
}
fun `test show dialog when explicit switch to K1`() {
val instance = createInstance()
instance.state.switchedToK1 = true
// instance.k2PluginModeForTests = false // it's not needed because the first branch with switchedToK1 should work
assertFalse(instance.state.userSawSurvey)
assertTrue(instance.shouldShowK2FeedbackDialog())
}
fun `test don't show dialog if less than one day on K2`() {
val instance = createInstance()
instance.state.k2UserSince = Instant.now().epochSecond.minus(1 * 60 * 60) // They've been on K2 just for 1 hour
assertFalse(instance.state.switchedToK1)
assertFalse(instance.state.userSawSurvey)
assertFalse(instance.shouldShowK2FeedbackDialog())
assertFalse(instance.state.userSawSurvey)
}
fun `test show dialog if more than one day on K2`() {
val instance = createInstance()
instance.state.k2UserSince = Instant.now().epochSecond.minus(25 * 60 * 60) // They've been on K2 for 25 hours
assertFalse(instance.state.switchedToK1)
assertFalse(instance.state.userSawSurvey)
assertTrue(instance.shouldShowK2FeedbackDialog())
}
}
@@ -0,0 +1,9 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.configuration
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import java.util.*
interface KotlinPluginKindSwitcherListener : EventListener {
fun kotlinPluginKindChanged(kotlinPluginMode: KotlinPluginMode)
}
@@ -13,10 +13,12 @@ import com.intellij.ui.dsl.builder.AlignY
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.EventDispatcher
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginKindSwitcher
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginModeProvider
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginKindSwitcher
import org.jetbrains.kotlin.idea.base.plugin.getPluginModeDescription
import org.jetbrains.kotlin.idea.configuration.KotlinPluginKindSwitcherListener
import org.jetbrains.kotlin.idea.preferences.KotlinPreferencesBundle
import javax.swing.JComponent
@@ -32,6 +34,11 @@ internal class KotlinPluginKindSwitcherController {
private lateinit var pluginTypeChooserPanel: Panel
private lateinit var currentPluginPanel: Panel
private val pluginKindChangedDispatcher: EventDispatcher<KotlinPluginKindSwitcherListener> =
EventDispatcher.create(
KotlinPluginKindSwitcherListener::class.java
)
private val productName: @NlsSafe String
get() = ApplicationNamesInfo.getInstance().fullProductName
@@ -42,6 +49,7 @@ internal class KotlinPluginKindSwitcherController {
fun applyChanges() {
KotlinPluginKindSwitcher.setPluginKindByVmOptions(chosenKind)
updatePanels()
pluginKindChangedDispatcher.multicaster.kotlinPluginKindChanged(chosenKind)
if (pluginKindWillBeSwitchedAfterRestart) {
suggestRestart()
}