IJPL-246739 Add in-IDE WSL user satisfaction survey

Adds a feedback survey targeted at users actively working with WSL to measure satisfaction with WSL support (performance and freezes in particular) and gather areas for improvement.

`WslSatisfactionSurveyStore` persists a distinct-days counter (incremented at most once per day by `WslSatisfactionSurveyProjectActivity` on WSL project open) and the first-WSL-use timestamp; `shouldShowDialog()` requires the project to have been opened in WSL on at least 3 distinct days and at least 3 days since first WSL use, matching the "opened in WSL x3 / after 3-5 days" brief.

The survey shows only on Windows in IDEA, PyCharm, WebStorm, PhpStorm and GoLand, only for version 2026.2(.x), and only for existing users (`InitialConfigImportState.isNewUser()` is excluded). Feedback auto-attaches IDE name, WSL distribution and version, and whether WSL runs via IJent; freezes are covered by the survey questions rather than auto-detected.

Registry keys `wsl.survey.first.use.date` and `wsl.survey.days.with.wsl` allow testing the show conditions, and an internal action shows the notification (and, via its "Respond" action, the dialog) as a test request.

The dialog uses the platform feedback UI blocks: a star `RatingBlock` for overall satisfaction and a star `RatingGroupBlock` for stability, responsiveness and overall quality, dropdowns for the two freeze-frequency questions, and a two-column checkbox group for where freezes occur. To support this, `RatingBlock` gains an optional hint, `ComboBoxBlock` gains `useWrappingLabel()` so a long question wraps instead of stretching the dialog, and `CheckBoxGroupBlock` gains `setColumnCount()` to lay options out over several columns (isolated in its own grid so it does not affect sibling blocks).

(cherry picked from commit 52635e9c8bd195ba3f97ef5f9f2f5cec72588d22)

IJ-MR-214055

GitOrigin-RevId: c5ee3239cfa3463e846173c758652c0615677fb8
This commit is contained in:
Alexander Koshevoy
2026-08-03 13:46:37 +00:00
committed by intellij-monorepo-bot
parent e2a6d9082d
commit aae05ffee9
15 changed files with 620 additions and 17 deletions
+2
View File
@@ -21,6 +21,7 @@ jvm_library(
deps = [
# do not sort,
"//platform/core-api:core",
"//platform/execution",
"//platform/projectModel-api:projectModel",
"//platform/util",
"//platform/editor-ui-api:editor-ui",
@@ -56,6 +57,7 @@ jvm_library(
# do not sort,
":feedback",
"//platform/core-api:core_test_lib",
"//platform/execution:execution_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/util:util_test_lib",
"//platform/editor-ui-api:editor-ui_test_lib",
@@ -30,6 +30,7 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.execution" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
@@ -8,6 +8,7 @@
<module name="intellij.platform.core.impl"/>
<module name="intellij.platform.core.ui"/>
<module name="intellij.platform.editor.ui"/>
<module name="intellij.platform.execution"/>
<module name="intellij.platform.ide.core"/>
<module name="intellij.platform.ide.impl"/>
<module name="intellij.platform.lang"/>
@@ -69,6 +70,14 @@
<feedback.idleFeedbackSurvey implementation="com.intellij.platform.feedback.editor.ux.EditorUxFeedbackSurvey"/>
<registryKey key="editor.ux.survey.enabled" defaultValue="false" description="Enables Editor UX survey for 10% users"/>
<feedback.idleFeedbackSurvey implementation="com.intellij.platform.feedback.wsl.WslSatisfactionSurvey"/>
<backgroundPostStartupActivity implementation="com.intellij.platform.feedback.wsl.WslSatisfactionSurveyProjectActivity"/>
<applicationService serviceImplementation="com.intellij.platform.feedback.wsl.WslSatisfactionSurveyStore"/>
<registryKey key="wsl.survey.first.use.date" defaultValue=""
description="ISO Formatted date of first WSL usage such as 2026-07-15 (for testing the WSL satisfaction survey)"/>
<registryKey key="wsl.survey.days.with.wsl" defaultValue="-1"
description="Overrides the number of days a WSL project was open (for testing the WSL satisfaction survey); -1 disables the override"/>
<registryKey key="csat.survey.enabled" defaultValue="true" description="Enables periodic CSAT survey"/>
<registryKey key="csat.survey.today" defaultValue="" description="ISO Formatted today date such as 2011-12-03"/>
<registryKey key="csat.survey.user.created.date" defaultValue="" description="ISO Formatted date when user created such as 2011-12-03"/>
@@ -78,6 +87,7 @@
<applicationSettings service="com.intellij.platform.feedback.localization.LocalizationFeedbackService"/>
<applicationSettings service="com.intellij.platform.feedback.impl.state.DontShowAgainFeedbackService"/>
<applicationSettings service="com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService"/>
<applicationSettings service="com.intellij.platform.feedback.wsl.WslSatisfactionSurveyStore"/>
</extensions>
<actions>
@@ -96,6 +106,9 @@
<action id="Editor.ShowEditorUxFeedbackNotification"
class="com.intellij.platform.feedback.editor.ux.ShowEditorUxFeedbackNotificationAction"
internal="true"/>
<action id="ShowWslSatisfactionDialog"
class="com.intellij.platform.feedback.wsl.ShowWslSatisfactionDialogAction"
internal="true"/>
<add-to-group group-id="Internal.UI" anchor="last"/>
</group>
@@ -0,0 +1,51 @@
action.ShowWslSatisfactionDialog.text=Show WSL Satisfaction Notification
wsl.satisfaction.notification.request.title=WSL feedback
wsl.satisfaction.notification.request.content=Please help us improve the WSL experience by taking our short survey.
wsl.satisfaction.dialog.top.title=WSL Support Experience Survey
wsl.satisfaction.dialog.title=WSL support experience survey
wsl.satisfaction.dialog.description=We are working on improving WSL support in {0} and would like to hear about your recent experience.
wsl.satisfaction.dialog.rating.label=How satisfied are you with WSL support?
wsl.satisfaction.dialog.rating.hint=Where 1 is "very dissatisfied" and 5 is "very satisfied".
wsl.satisfaction.dialog.areas.label=How would you rate the following aspects of WSL support?
wsl.satisfaction.dialog.areas.hint=Where 1 is "poor" and 5 is "excellent".
wsl.satisfaction.dialog.area.stability=Stability (freezes)
wsl.satisfaction.dialog.area.responsiveness=Responsiveness
wsl.satisfaction.dialog.area.quality=Overall quality
wsl.satisfaction.dialog.freeze.comparison.label=Compared to your previous experience, how often does the IDE freeze, hang, or become unresponsive when working with WSL in the current version?
wsl.satisfaction.dialog.freeze.comparison.less=Less often
wsl.satisfaction.dialog.freeze.comparison.same=About the same as before
wsl.satisfaction.dialog.freeze.comparison.more=More often
wsl.satisfaction.dialog.freeze.comparison.unsure=Hard to say
wsl.satisfaction.dialog.freeze.frequency.label=How often do you currently experience IDE freezes when working with WSL?
wsl.satisfaction.dialog.freeze.frequency.several.day=Several times a day
wsl.satisfaction.dialog.freeze.frequency.once.day=About once a day
wsl.satisfaction.dialog.freeze.frequency.several.week=Several times a week
wsl.satisfaction.dialog.freeze.frequency.monthly=Once or several times a month
wsl.satisfaction.dialog.freeze.frequency.rarely=Rarely or almost never
wsl.satisfaction.dialog.freeze.situations.label=When do freezes usually occur, if at all?
wsl.satisfaction.dialog.freeze.situations.none=I don't encounter freezes
wsl.satisfaction.dialog.freeze.situations.opening=Opening a WSL project
wsl.satisfaction.dialog.freeze.situations.indexing=Indexing
wsl.satisfaction.dialog.freeze.situations.running=Running or debugging
wsl.satisfaction.dialog.freeze.situations.terminal=Using the terminal
wsl.satisfaction.dialog.freeze.situations.vcs=Git/VCS operations
wsl.satisfaction.dialog.freeze.situations.docker=Docker or containers
wsl.satisfaction.dialog.freeze.situations.editing=Editing files
wsl.satisfaction.dialog.freeze.situations.branches=Switching branches
wsl.satisfaction.dialog.freeze.situations.search=Searching/navigation
wsl.satisfaction.dialog.additional.label=Is there anything else you would like to tell us about WSL support in {0}?
wsl.satisfaction.thanks.feedback.content=Thank you for your feedback! It will help us improve WSL support.
wsl.satisfaction.system.info.ide.name=IDE name:
wsl.satisfaction.system.info.distribution=WSL distribution:
wsl.satisfaction.system.info.wsl.version=WSL version:
wsl.satisfaction.system.info.eel.api.used=WSL via IJent:
@@ -11,6 +11,7 @@ import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.layout.selected
@@ -34,6 +35,7 @@ class CheckBoxGroupBlock(
private var myIncludeOtherTextField = false
private var myOtherProperty: String = ""
private var myOtherTextfieldPlaceholderText: String = ""
private var myColumnCount: Int = 1
private var otherCheckBox: JBCheckBox? = null
private var otherTextField: JBTextField? = null
@@ -41,23 +43,31 @@ class CheckBoxGroupBlock(
override fun addToPanel(panel: Panel) {
val allCheckBoxes = mutableListOf<JBCheckBox>()
panel.apply {
buttonsGroup(indent = false) {
fun Row.addCheckBox(itemData: CheckBoxItemData) {
allCheckBoxes += checkBox(itemData.label)
.bindSelected({ itemData.property }, { itemData.property = it })
.addChooseOptionValidation(allCheckBoxes)
.component
}
val itemRows = myItemsData.chunked(myColumnCount)
val groupContent: (Panel) -> Unit = { container ->
container.buttonsGroup(indent = false) {
row {
label(myGroupLabel)
.bold()
}.bottomGap(BottomGap.NONE)
myItemsData.forEachIndexed { i, itemData ->
row {
allCheckBoxes += checkBox(itemData.label).bindSelected(
{ myItemsData[i].property },
{ myItemsData[i].property = it }
).addChooseOptionValidation(allCheckBoxes)
.component
}.apply {
if (i == myItemsData.size - 1 && !myIncludeOtherTextField) {
this.bottomGap(BottomGap.MEDIUM)
}
itemRows.forEachIndexed { rowIndex, rowItems ->
// Use the standard multi-column DSL helpers so the checkboxes align in evenly spaced columns.
val cells: List<(Row.() -> Unit)?> = rowItems.map { itemData -> { addCheckBox(itemData) } }
val addedRow = when (myColumnCount) {
2 -> twoColumnsRow(cells.getOrNull(0), cells.getOrNull(1))
3 -> threeColumnsRow(cells.getOrNull(0), cells.getOrNull(1), cells.getOrNull(2))
else -> row { rowItems.forEach { addCheckBox(it) } }
}
if (rowIndex == itemRows.lastIndex && !myIncludeOtherTextField) {
addedRow.bottomGap(BottomGap.MEDIUM)
}
}
@@ -109,6 +119,17 @@ class CheckBoxGroupBlock(
}
}
}
panel.apply {
if (myColumnCount > 1) {
// Isolate the multi-column grid in its own sub-panel so its wide columns do not force sibling
// blocks (e.g. rating rows) to align with them.
panel { groupContent(this) }
}
else {
groupContent(this)
}
}
}
override fun collectBlockTextDescription(stringBuilder: StringBuilder) {
@@ -151,6 +172,22 @@ class CheckBoxGroupBlock(
return this
}
/**
* Lays the checkboxes out over the given number of [columnCount] columns (1, 2 or 3) to keep tall option
* lists compact, using the standard [Panel.twoColumnsRow]/[Panel.threeColumnsRow] layout.
*
* Follow the IntelliJ UI guidelines when choosing the value:
* arrange checkboxes with labels of up to 30 characters in 2 columns,
* and checkboxes with labels of up to 15 characters in 3 columns.
*
* @see <a href="https://plugins.jetbrains.com/docs/intellij/layout.html#checkboxes-and-radio-buttons">Layout guidelines</a>
*/
fun setColumnCount(columnCount: Int): CheckBoxGroupBlock {
require(columnCount in 1..3) { "columnCount must be 1, 2 or 3, but was $columnCount" }
myColumnCount = columnCount
return this
}
private fun <T : JBCheckBox> Cell<T>.addChooseOptionValidation(allCheckBoxes: List<JBCheckBox>): Cell<T> {
if (requireAnswer) {
val errorMessage = if (myIncludeOtherTextField) {
@@ -8,10 +8,12 @@ import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.LabelPosition
import com.intellij.ui.dsl.builder.MAX_LINE_LENGTH_WORD_WRAP
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.toMutableProperty
import com.intellij.xml.util.XmlStringUtil
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
@@ -24,14 +26,26 @@ class ComboBoxBlock(@NlsContexts.Label private val myLabel: String,
private var myColumnSize: Int = COMBOBOX_COLUMN_SIZE
private var myRandomizeOptionOrder: Boolean = false
private var myUseAlignFill: Boolean = false
private var myUseWrappingLabel: Boolean = false
override fun addToPanel(panel: Panel) {
val items = if (myRandomizeOptionOrder) myItems.shuffled() else myItems
panel.apply {
// A long label rendered as a single-line JBLabel forces the whole dialog to become very wide,
// so it can optionally be shown as a word-wrapped label above the combo box instead.
if (myUseWrappingLabel) {
row {
text("<b>${XmlStringUtil.escapeString(myLabel)}</b>", maxLineLength = MAX_LINE_LENGTH_WORD_WRAP)
}.bottomGap(BottomGap.NONE)
}
row {
comboBox(items)
.label(createBoldJBLabel(myLabel), LabelPosition.TOP)
.apply {
if (!myUseWrappingLabel) {
label(createBoldJBLabel(myLabel), LabelPosition.TOP)
}
}
.bindItem(::myProperty.toMutableProperty())
.apply {
if (myUseAlignFill) {
@@ -82,6 +96,15 @@ class ComboBoxBlock(@NlsContexts.Label private val myLabel: String,
return this
}
/**
* Renders the label as a word-wrapped text above the combo box instead of a single-line label,
* so a long question does not stretch the whole dialog horizontally.
*/
fun useWrappingLabel(): ComboBoxBlock {
myUseWrappingLabel = true
return this
}
fun randomizeOptionOrder(): ComboBoxBlock {
myRandomizeOptionOrder = true
return this
@@ -12,18 +12,35 @@ import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.Row
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
import org.jetbrains.annotations.Nls
class RatingBlock(@NlsContexts.Label private val myLabel: String,
private val myJsonElementName: String) : FeedbackBlock, TextDescriptionProvider, JsonDataProvider {
class RatingBlock(
@NlsContexts.Label private val myLabel: String,
private val myJsonElementName: String,
) : FeedbackBlock, TextDescriptionProvider, JsonDataProvider {
private var myProperty: Int = 0
private var requireAnswer: Boolean = true
private var myHint: @Nls String? = null
override fun addToPanel(panel: Panel) {
val hint = myHint
panel.apply {
// With a hint, show the label and the hint above the rating (like RatingGroupBlock);
// otherwise keep the label attached on top of the rating for backward compatibility.
if (hint != null) {
row {
label(myLabel).bold()
rowComment(hint)
}.bottomGap(BottomGap.NONE)
}
row {
rating()
.label(createBoldJBLabel(myLabel), LabelPosition.TOP)
.apply {
if (hint == null) {
label(createBoldJBLabel(myLabel), LabelPosition.TOP)
}
}
.apply {
onApply {
myProperty = this.component.myRating
@@ -57,6 +74,11 @@ class RatingBlock(@NlsContexts.Label private val myLabel: String,
requireAnswer = false
return this
}
fun setHint(@Nls hint: String): RatingBlock {
myHint = hint
return this
}
}
internal fun Row.rating(): Cell<RatingComponent> {
@@ -0,0 +1,27 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import java.util.function.Supplier
/**
* Shows the WSL satisfaction survey notification (bypassing the usual show conditions) so QA can check
* both the notification wording and, via the "Respond" action, the dialog. Feedback is sent as a test request.
*/
@Suppress("ActionPresentationInstantiatedInCtor")
internal class ShowWslSatisfactionDialogAction :
DumbAwareAction(Supplier { WslSatisfactionFeedbackBundle.message("action.ShowWslSatisfactionDialog.text") }) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
WslSatisfactionSurvey().showNotification(project, forTest = true)
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
}
@@ -0,0 +1,20 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
internal object WslSatisfactionFeedbackBundle {
@NonNls
private const val BUNDLE = "messages.WslSatisfactionFeedbackBundle"
private val INSTANCE = DynamicBundle(WslSatisfactionFeedbackBundle::class.java, BUNDLE)
@Nls
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
return INSTANCE.getMessage(key, *params)
}
}
@@ -0,0 +1,176 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.execution.wsl.WSLDistribution
import com.intellij.execution.wsl.WslDistributionManager
import com.intellij.execution.wsl.WslIjentAvailabilityService
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.WindowManager
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.CheckBoxGroupBlock
import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxItemData
import com.intellij.platform.feedback.dialog.uiBlocks.ComboBoxBlock
import com.intellij.platform.feedback.dialog.uiBlocks.DescriptionBlock
import com.intellij.platform.feedback.dialog.uiBlocks.FeedbackBlock
import com.intellij.platform.feedback.dialog.uiBlocks.RatingBlock
import com.intellij.platform.feedback.dialog.uiBlocks.RatingGroupBlock
import com.intellij.platform.feedback.dialog.uiBlocks.RatingItem
import com.intellij.platform.feedback.dialog.uiBlocks.TextAreaBlock
import com.intellij.platform.feedback.dialog.uiBlocks.TopLabelBlock
import com.intellij.platform.feedback.impl.notification.ThanksForFeedbackNotification
import com.intellij.ui.ScreenUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.Dimension
import java.awt.Rectangle
import java.nio.file.Path
import javax.swing.JComponent
internal class WslSatisfactionFeedbackDialog(
private val project: Project,
forTest: Boolean,
) : BlockBasedFeedbackDialogWithEmail<WslSatisfactionSystemData>(project, forTest) {
/** Increase the additional number when feedback format is changed */
override val myFeedbackJsonVersion: Int = super.myFeedbackJsonVersion + 1
override val myFeedbackReportId: String = "wsl_satisfaction"
override val zendeskTicketTitle: String = "WSL Support in-IDE Feedback"
override val zendeskFeedbackType: String = "WSL Support Feedback"
override fun shouldAutoCloseZendeskTicket(): Boolean = false
private val ideName: String = ApplicationNamesInfo.getInstance().fullProductName
override suspend fun computeSystemInfoData(): WslSatisfactionSystemData {
val distribution = withContext(Dispatchers.IO) { findWslDistribution(project.basePath) }
val isEelApiUsed = WslIjentAvailabilityService.getInstance().runWslCommandsViaIjent()
return WslSatisfactionSystemData(
ideName = ideName,
wslDistribution = distribution?.presentableName ?: "UNKNOWN",
wslVersion = distribution?.version ?: -1,
isEelApiUsed = isEelApiUsed,
commonData = CommonFeedbackSystemData.getCurrentData(),
)
}
private fun findWslDistribution(projectPath: String?): WSLDistribution? {
if (projectPath == null) return null
return WslDistributionManager.getInstance().installedDistributions.firstOrNull {
runCatching { Path.of(projectPath).startsWith(Path.of(it.getUNCRootPath().toUri())) }.getOrDefault(false)
}
}
override fun showFeedbackSystemInfoDialog(systemInfoData: WslSatisfactionSystemData) {
showFeedbackSystemInfoDialog(myProject, systemInfoData.commonData) {
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.ide.name")) {
label(systemInfoData.ideName)
}
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.distribution")) {
label(systemInfoData.wslDistribution)
}
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.wsl.version")) {
label(systemInfoData.wslVersion.toString())
}
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.eel.api.used")) {
label(systemInfoData.isEelApiUsed.toString())
}
}
}
override val myTitle: String = WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.top.title")
override val myBlocks: List<FeedbackBlock> = listOf(
TopLabelBlock(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.title")),
DescriptionBlock(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.description", ideName)),
RatingBlock(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.rating.label"),
"satisfaction",
).setHint(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.rating.hint")),
RatingGroupBlock(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.areas.label"),
listOf(
RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.stability"), "stability"),
RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.responsiveness"), "responsiveness"),
RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.quality"), "overall_quality"),
),
).setHint(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.areas.hint")),
ComboBoxBlock(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.label"),
listOf(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.less"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.same"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.more"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.unsure"),
),
"freeze_comparison",
).useWrappingLabel(),
ComboBoxBlock(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.label"),
listOf(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.several.day"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.once.day"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.several.week"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.monthly"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.rarely"),
),
"freeze_frequency",
).useWrappingLabel(),
CheckBoxGroupBlock(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.label"),
listOf(
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.none"), "no_freezes"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.opening"), "opening_project"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.indexing"), "indexing"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.running"), "running_debugging"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.terminal"), "terminal"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.vcs"), "vcs"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.docker"), "docker"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.editing"), "editing"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.branches"), "switching_branches"),
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.search"), "search_navigation"),
),
"freeze_situations",
).setColumnCount(2).addOtherTextField(),
TextAreaBlock(
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.additional.label", ideName),
"additional_feedback",
),
)
init {
init()
}
override fun createCenterPanel(): JComponent {
val centerPanel = super.createCenterPanel()
// The survey is long; cap the default height at 80% of the target screen (per the platform window-size
// guidelines) and let the content scroll, instead of growing the dialog to the full content height.
val maxHeight = (targetScreenBounds().height * 0.8).toInt()
val preferred = centerPanel.preferredSize
if (preferred.height > maxHeight) {
centerPanel.preferredSize = Dimension(preferred.width, maxHeight)
}
return centerPanel
}
/** Bounds of the screen the dialog will appear on (it is centered over the parent project frame). */
private fun targetScreenBounds(): Rectangle {
val frame = myProject?.let { WindowManager.getInstance().getFrame(it) }
return if (frame != null) ScreenUtil.getScreenRectangle(frame) else ScreenUtil.getMainScreenBounds()
}
override fun showThanksNotification() {
ThanksForFeedbackNotification(
description = WslSatisfactionFeedbackBundle.message("wsl.satisfaction.thanks.feedback.content")
).notify(myProject)
}
}
@@ -0,0 +1,12 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.platform.feedback.FeedbackSurvey
import com.intellij.platform.feedback.FeedbackSurveyType
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
import com.intellij.platform.feedback.InIdeFeedbackSurveyType
internal class WslSatisfactionSurvey : FeedbackSurvey() {
override val feedbackSurveyType: FeedbackSurveyType<InIdeFeedbackSurveyConfig> =
InIdeFeedbackSurveyType(WslSatisfactionSurveyConfig())
}
@@ -0,0 +1,72 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.InitialConfigImportState
import com.intellij.openapi.project.Project
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification
import com.intellij.util.PlatformUtils
import com.intellij.util.system.LowLevelLocalMachineAccess
import com.intellij.util.system.OS
import kotlinx.datetime.LocalDate
internal class WslSatisfactionSurveyConfig : InIdeFeedbackSurveyConfig {
override val surveyId: String = "wsl_satisfaction"
// Survey is targeted at 2026.2 + 2026.2.x; collect for ~two months after release. Adjust to the actual release date.
override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2026, 10, 1)
override val requireIdeEAP: Boolean = false
@OptIn(LowLevelLocalMachineAccess::class)
override fun checkIdeIsSuitable(): Boolean {
// WSL exists only on Windows; the brief limits the survey to these IDEs.
return OS.CURRENT == OS.Windows &&
(PlatformUtils.isIdeaUltimate() ||
PlatformUtils.isIdeaCommunity() ||
PlatformUtils.isPyCharm() ||
PlatformUtils.isWebStorm() ||
PlatformUtils.isPhpStorm() ||
PlatformUtils.isGoIde())
}
override fun checkExtraConditionSatisfied(project: Project): Boolean {
if (!isTargetVersion()) return false
// Don't show for new users - only for existing users who upgraded to the new version.
if (InitialConfigImportState.isNewUser()) return false
if (!project.isWslProject()) return false
return WslSatisfactionSurveyStore.getInstance().shouldShowDialog()
}
private fun isTargetVersion(): Boolean {
return ApplicationInfo.getInstance().shortVersion == TARGET_VERSION
}
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> {
return WslSatisfactionFeedbackDialog(project, forTest)
}
override fun updateStateAfterDialogClosedOk(project: Project) {
WslSatisfactionSurveyStore.getInstance().recordSurveyShown()
}
override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification {
return RequestFeedbackNotification(
"Feedback In IDE",
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.notification.request.title"),
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.notification.request.content"),
)
}
override fun updateStateAfterNotificationShowed(project: Project) {
// do nothing
}
companion object {
private const val TARGET_VERSION: String = "2026.2"
}
}
@@ -0,0 +1,16 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
/**
* Records that a WSL project was opened, feeding the "days working with WSL" counter used by
* [WslSatisfactionSurveyConfig] to decide when to show the WSL satisfaction survey.
*/
internal class WslSatisfactionSurveyProjectActivity : ProjectActivity {
override suspend fun execute(project: Project) {
if (!project.isWslProject()) return
WslSatisfactionSurveyStore.getInstance().recordWslProjectOpened()
}
}
@@ -0,0 +1,106 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.system.LowLevelLocalMachineAccess
import com.intellij.util.system.OS
import java.time.Duration
import java.time.Instant
private val WSL_PATH_PREFIXES = listOf("//wsl$/", "//wsl.localhost/")
/**
* Whether [this] project is opened from a WSL file system.
*
* Uses the same lightweight base-path prefix check as
* [com.intellij.featureStatistics.fusCollectors.WslUsagesCollector], so it can be called
* on any thread without touching the (potentially slow) distribution manager.
*/
@OptIn(LowLevelLocalMachineAccess::class)
internal fun Project.isWslProject(): Boolean {
if (OS.CURRENT != OS.Windows) return false
val projectPath = guessProjectDir()?.path ?: return false
return WSL_PATH_PREFIXES.any { projectPath.startsWith(it) }
}
internal class WslSatisfactionSurveyState : BaseState() {
// Unix time seconds of the first detected WSL project open.
var firstWslUseTime: Long by property(0L)
// Number of distinct days on which a WSL project was open.
var daysWithWslProject: Int by property(0)
// Unix time seconds; [daysWithWslProject] is incremented at most once after this moment.
var nextCountedDay: Long by property(0L)
var userSawSurvey: Boolean by property(false)
}
@State(name = "WslSatisfactionSurveyStore", storages = [Storage(value = "wsl-survey.xml", roamingType = RoamingType.DISABLED)])
internal class WslSatisfactionSurveyStore : PersistentStateComponent<WslSatisfactionSurveyState> {
internal var currentState: WslSatisfactionSurveyState = WslSatisfactionSurveyState()
override fun getState(): WslSatisfactionSurveyState = currentState
override fun loadState(state: WslSatisfactionSurveyState) {
currentState = state
}
internal fun recordSurveyShown() {
currentState.userSawSurvey = true
}
/**
* Records that a WSL project was open now. Increments [WslSatisfactionSurveyState.daysWithWslProject]
* at most once per calendar day and stamps the first-usage time.
*/
internal fun recordWslProjectOpened() {
val now = Instant.now()
if (now >= Instant.ofEpochSecond(currentState.nextCountedDay)) {
currentState.daysWithWslProject++
currentState.nextCountedDay = (now + Duration.ofDays(1)).epochSecond
}
if (currentState.firstWslUseTime == 0L) {
currentState.firstWslUseTime = now.epochSecond
}
}
internal fun shouldShowDialog(): Boolean {
val state = currentState
if (state.userSawSurvey) return false
val daysWithWslProject = Registry.intValue(REGISTRY_DAYS_WITH_WSL, -1).takeIf { it >= 0 } ?: state.daysWithWslProject
if (daysWithWslProject < MINIMUM_DAYS_WITH_WSL_PROJECT) return false
val firstWslUseTime = firstWslUseTime() ?: return false
return Duration.between(firstWslUseTime, Instant.now()) >= MINIMUM_DURATION_SINCE_FIRST_USE
}
private fun firstWslUseTime(): Instant? {
val override = Registry.stringValue(REGISTRY_FIRST_USE_DATE)
if (override.isNotBlank()) {
return runCatching { java.time.LocalDate.parse(override).atStartOfDay(java.time.ZoneOffset.UTC).toInstant() }.getOrNull()
}
if (currentState.firstWslUseTime == 0L) return null
return Instant.ofEpochSecond(currentState.firstWslUseTime)
}
companion object {
// "Project opened in WSL x3" + "after 3-5 days working with WSL" (IJPL-246739 brief).
internal const val MINIMUM_DAYS_WITH_WSL_PROJECT: Int = 3
internal val MINIMUM_DURATION_SINCE_FIRST_USE: Duration = Duration.ofDays(3)
internal const val REGISTRY_FIRST_USE_DATE: String = "wsl.survey.first.use.date"
internal const val REGISTRY_DAYS_WITH_WSL: String = "wsl.survey.days.with.wsl"
fun getInstance(): WslSatisfactionSurveyStore = service()
}
}
@@ -0,0 +1,25 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.wsl
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement
/**
* Data attached automatically to the WSL satisfaction survey feedback, in addition to the answers:
* IDE name, WSL distribution and version, and whether WSL commands run via the IJent/EEL API.
*/
@Serializable
internal data class WslSatisfactionSystemData(
@NlsSafe val ideName: String,
@NlsSafe val wslDistribution: String,
val wslVersion: Int,
val isEelApiUsed: Boolean,
val commonData: CommonFeedbackSystemData,
) : SystemDataJsonSerializable {
override fun serializeToJson(json: Json): JsonElement = json.encodeToJsonElement(this)
}