mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-22 06:21:25 +07:00
[feedback] IJPL-177421 Permanent CSAT survey in IDEs
Show next CSAT date via internal action (cherry picked from commit 5239a1bb889eb72c03880c0e4eaaf4bf2291efd1) (cherry picked from commit 52deaf5498f65c4d613cbef89ef972cc453105cd) IJ-MR-155667 GitOrigin-RevId: fc385859e5bc773bbe07c4518937b82815d8d2d9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f0b1bb4c47
commit
0c96b4504c
@@ -72,5 +72,7 @@
|
||||
<action class="com.intellij.platform.feedback.demo.ShowInIdeDemoFeedbackWithStatsAction" internal="true"/>
|
||||
<action class="com.intellij.platform.feedback.csat.CsatFeedbackAction" internal="true"
|
||||
text="Show CSAT Feedback Dialog"/>
|
||||
<action class="com.intellij.platform.feedback.csat.CsatFeedbackNextDayAction" internal="true"
|
||||
text="Show Next Date for CSAT Feedback Dialog"/>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
@@ -24,6 +24,7 @@ dialog.feedback.system.info.panel.disabled.plugins=Disabled bundled plugins:
|
||||
dialog.feedback.system.info.panel.disabled.plugins.empty=None
|
||||
dialog.feedback.system.info.panel.nonbundled.plugins=Non-bundled plugins:
|
||||
dialog.feedback.system.info.panel.nonbundled.plugins.empty=None
|
||||
dialog.feedback.system.info.panel.remote.dev.host=Remote development host:
|
||||
|
||||
dialog.feedback.ok.label=Send Feedback
|
||||
dialog.feedback.cancel.label=Cancel
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.csat
|
||||
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
internal class CsatFeedbackAction : AnAction() {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
@@ -11,4 +14,22 @@ internal class CsatFeedbackAction : AnAction() {
|
||||
CsatFeedbackSurvey().showNotification(project, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class CsatFeedbackNextDayAction : AnAction() {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project
|
||||
if (project != null) {
|
||||
val nextDate = getNextCsatDay()
|
||||
|
||||
NotificationGroupManager.getInstance().getNotificationGroup("System Messages")
|
||||
.createNotification(
|
||||
"Next CSAT feedback day is " + nextDate.date.format(DateTimeFormatter.ISO_DATE) +
|
||||
"User is${if (!nextDate.isNewUser) " not " else " "}new.",
|
||||
NotificationType.INFORMATION
|
||||
)
|
||||
.notify(project)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ import com.intellij.platform.feedback.dialog.uiBlocks.FeedbackBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.SegmentedButtonBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.TextAreaBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.TopLabelBlock
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
|
||||
@Serializable
|
||||
internal data class CsatFeedbackSystemData(
|
||||
val isNewUser: Boolean,
|
||||
val systemInfo: CommonFeedbackSystemData
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.intellij.internal.statistic.eventLog.fus.MachineIdManager
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.application.ConfigImportHelper
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.feedback.FeedbackSurvey
|
||||
@@ -18,6 +17,7 @@ import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotificat
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Month
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -25,6 +25,7 @@ internal const val USER_CONSIDERED_NEW_DAYS = 30
|
||||
internal const val NEW_USER_SURVEY_PERIOD = 29
|
||||
internal const val EXISTING_USER_SURVEY_PERIOD = 97
|
||||
internal const val CSAT_SURVEY_LAST_FEEDBACK_DATE_KEY = "csat.survey.last.feedback.date"
|
||||
internal const val CSAT_SURVEY_LAST_NOTIFICATION_DATE_KEY = "csat.survey.last.notification.date"
|
||||
|
||||
internal class CsatFeedbackSurvey : FeedbackSurvey() {
|
||||
override val feedbackSurveyType: InIdeFeedbackSurveyType<InIdeFeedbackSurveyConfig> =
|
||||
@@ -38,6 +39,7 @@ internal class CsatFeedbackSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
override val surveyId: String = "csat_feedback"
|
||||
override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2050, Month.JANUARY, 1)
|
||||
override val requireIdeEAP: Boolean = false
|
||||
override val isIndefinite: Boolean = true
|
||||
|
||||
override fun checkIdeIsSuitable(): Boolean = Registry.`is`("csat.survey.enabled")
|
||||
|
||||
@@ -50,13 +52,25 @@ internal class CsatFeedbackSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
}
|
||||
|
||||
override fun checkExtraConditionSatisfied(project: Project): Boolean {
|
||||
if (ConfigImportHelper.isFirstSession()) return false
|
||||
if (ConfigImportHelper.isFirstSession()) {
|
||||
LOG.debug("It's a first user session, skip the survey")
|
||||
return false
|
||||
}
|
||||
|
||||
val today = getCsatToday()
|
||||
LOG.debug("Today is ${today.format(DateTimeFormatter.ISO_LOCAL_DATE)}")
|
||||
|
||||
PropertiesComponent.getInstance().getValue(CSAT_SURVEY_LAST_NOTIFICATION_DATE_KEY)
|
||||
?.let { tryParseDate(it) }
|
||||
?.let {
|
||||
if (it.isEqual(today)) {
|
||||
LOG.debug("Already notified today, skip the survey")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val lastFeedbackDate = PropertiesComponent.getInstance().getValue(CSAT_SURVEY_LAST_FEEDBACK_DATE_KEY)
|
||||
?.let { java.time.LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) }
|
||||
?.let { tryParseDate(it) }
|
||||
if (lastFeedbackDate != null && lastFeedbackDate.plusDays(EXISTING_USER_SURVEY_PERIOD.toLong()).isAfter(today)) {
|
||||
LOG.debug("User recently filled the survey, vacation period is in progress")
|
||||
return false
|
||||
@@ -70,10 +84,9 @@ internal class CsatFeedbackSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
LOG.debug("User is a new user")
|
||||
}
|
||||
|
||||
val surveyPeriod = if (isNewUser) NEW_USER_SURVEY_PERIOD else EXISTING_USER_SURVEY_PERIOD
|
||||
|
||||
val productHash = abs((ApplicationInfo.getInstance().versionName + MachineIdManager.getAnonymizedMachineId("CSAT Survey")).hashCode()) % surveyPeriod
|
||||
val daysHash = abs(ChronoUnit.DAYS.between(java.time.LocalDate.of(1970, 1, 1), today).toInt()) % surveyPeriod
|
||||
val surveyPeriod = getSurveyPeriod(isNewUser)
|
||||
val productHash = getProductHash(surveyPeriod)
|
||||
val daysHash = getDaysHash(today, surveyPeriod)
|
||||
|
||||
if (productHash != daysHash) {
|
||||
LOG.debug("Periods do not match: $productHash / $daysHash, is not yet suitable date for the survey")
|
||||
@@ -97,19 +110,54 @@ internal class CsatFeedbackSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
}
|
||||
|
||||
override fun updateStateAfterNotificationShowed(project: Project) {
|
||||
PropertiesComponent.getInstance().setValue(CSAT_SURVEY_LAST_NOTIFICATION_DATE_KEY,
|
||||
getCsatToday().format(DateTimeFormatter.ISO_LOCAL_DATE))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDaysHash(today: java.time.LocalDate, surveyPeriod: Int): Int {
|
||||
return abs(ChronoUnit.DAYS.between(java.time.LocalDate.of(1970, 1, 1), today).toInt()) % surveyPeriod
|
||||
}
|
||||
|
||||
private fun getProductHash(surveyPeriod: Int): Int {
|
||||
return abs((ApplicationInfo.getInstance().versionName + MachineIdManager.getAnonymizedMachineId("CSAT Survey")).hashCode()) % surveyPeriod
|
||||
}
|
||||
|
||||
internal data class NextDate(
|
||||
val isNewUser: Boolean,
|
||||
val date: java.time.LocalDate
|
||||
)
|
||||
|
||||
internal fun getNextCsatDay(): NextDate {
|
||||
val today = getCsatToday()
|
||||
val userCreatedDate = getCsatUserCreatedDate()
|
||||
|
||||
val isNewUser = userCreatedDate?.let { isNewUser(today, userCreatedDate) } ?: false
|
||||
val surveyPeriod = getSurveyPeriod(isNewUser)
|
||||
|
||||
for (i in 0..364) {
|
||||
val date = today.plusDays(i.toLong())
|
||||
|
||||
val productHash = getProductHash(surveyPeriod)
|
||||
val daysHash = getDaysHash(date, surveyPeriod)
|
||||
|
||||
if (productHash == daysHash) {
|
||||
return NextDate(isNewUser, date)
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalStateException("No suitable date found")
|
||||
}
|
||||
|
||||
private fun getSurveyPeriod(isNewUser: Boolean): Int {
|
||||
return if (isNewUser) NEW_USER_SURVEY_PERIOD else EXISTING_USER_SURVEY_PERIOD
|
||||
}
|
||||
|
||||
internal fun getCsatToday(): java.time.LocalDate {
|
||||
try {
|
||||
Registry.stringValue("csat.survey.today")
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { java.time.LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) }
|
||||
?.let { return it }
|
||||
}
|
||||
catch (e: Exception) {
|
||||
fileLogger().error(e)
|
||||
}
|
||||
Registry.stringValue("csat.survey.today")
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { tryParseDate(it) }
|
||||
?.let { return it }
|
||||
|
||||
return java.time.LocalDate.now()
|
||||
}
|
||||
@@ -156,4 +204,13 @@ internal fun flipACoin(productCode: String, newUser: Boolean): Boolean {
|
||||
|
||||
internal fun isNewUser(today: java.time.LocalDate, userCreatedDate: java.time.LocalDate): Boolean {
|
||||
return today.isBefore(userCreatedDate.plusDays(USER_CONSIDERED_NEW_DAYS.toLong()))
|
||||
}
|
||||
|
||||
internal fun tryParseDate(it: String): java.time.LocalDate? {
|
||||
return try {
|
||||
java.time.LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
}
|
||||
catch (_: DateTimeParseException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,10 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE
|
||||
|
||||
internal const val CSAT_NEW_USER_CREATED_AT_PROPERTY = "csat.user.created.at"
|
||||
|
||||
internal val CSAT_TS_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE
|
||||
|
||||
internal class CsatNewUserTracker : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
if (ConfigImportHelper.isNewUser()) {
|
||||
@@ -21,7 +18,7 @@ internal class CsatNewUserTracker : ProjectActivity {
|
||||
|
||||
if (!propertiesComponent.isValueSet(CSAT_NEW_USER_CREATED_AT_PROPERTY)) {
|
||||
propertiesComponent.setValue(CSAT_NEW_USER_CREATED_AT_PROPERTY,
|
||||
LocalDate.now().format(CSAT_TS_FORMAT))
|
||||
LocalDate.now().format(ISO_LOCAL_DATE))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,19 +27,9 @@ internal class CsatNewUserTracker : ProjectActivity {
|
||||
internal fun getCsatUserCreatedDate(): LocalDate? {
|
||||
val mocked = Registry.stringValue("csat.survey.user.created.date")
|
||||
if (!mocked.isBlank()) {
|
||||
return try {
|
||||
LocalDate.parse(mocked, CSAT_TS_FORMAT)
|
||||
}
|
||||
catch (_: DateTimeParseException) {
|
||||
null
|
||||
}
|
||||
return tryParseDate(mocked)
|
||||
}
|
||||
|
||||
val date = PropertiesComponent.getInstance().getValue(CSAT_NEW_USER_CREATED_AT_PROPERTY) ?: return null
|
||||
return try {
|
||||
LocalDate.parse(date, CSAT_TS_FORMAT)
|
||||
}
|
||||
catch (_: DateTimeParseException) {
|
||||
null
|
||||
}
|
||||
return tryParseDate(date)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package com.intellij.platform.feedback.dialog
|
||||
import com.intellij.ide.nls.NlsMessages
|
||||
import com.intellij.ide.plugins.IdeaPluginDescriptor
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.idea.AppMode
|
||||
import com.intellij.internal.statistic.utils.getPluginInfoById
|
||||
import com.intellij.internal.statistic.utils.platformPlugin
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
@@ -21,11 +22,12 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/** This number should be increased when [CommonFeedbackSystemData] fields changing */
|
||||
const val COMMON_FEEDBACK_SYSTEM_INFO_VERSION = 2
|
||||
const val COMMON_FEEDBACK_SYSTEM_INFO_VERSION: Int = 3
|
||||
|
||||
@Serializable
|
||||
data class CommonFeedbackSystemData(
|
||||
@@ -39,7 +41,8 @@ data class CommonFeedbackSystemData(
|
||||
private val isInternalModeEnabled: Boolean,
|
||||
private val registry: List<String>,
|
||||
private val disabledBundledPlugins: List<String>,
|
||||
private val nonBundledPlugins: List<String>
|
||||
private val nonBundledPlugins: List<String>,
|
||||
private val isRemoteDevelopmentHost: Boolean,
|
||||
) : SystemDataJsonSerializable {
|
||||
companion object {
|
||||
fun getCurrentData(): CommonFeedbackSystemData {
|
||||
@@ -54,7 +57,8 @@ data class CommonFeedbackSystemData(
|
||||
getIsInternalMode(),
|
||||
getRegistryKeys(),
|
||||
getDisabledPlugins(),
|
||||
getNonBundledPlugins()
|
||||
getNonBundledPlugins(),
|
||||
AppMode.isRemoteDevHost()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,8 +124,10 @@ data class CommonFeedbackSystemData(
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun getMemorySizeForDialog() = memorySize.toString() + "M"
|
||||
fun getLicenseRestrictionsForDialog() = if (licenseRestrictions.isEmpty())
|
||||
fun getMemorySizeForDialog(): String = memorySize.toString() + "M"
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
fun getLicenseRestrictionsForDialog(): @Nls String = if (licenseRestrictions.isEmpty())
|
||||
CommonFeedbackBundle.message("dialog.feedback.system.info.panel.license.no.info")
|
||||
else
|
||||
licenseRestrictions.joinToString("\n")
|
||||
@@ -201,6 +207,8 @@ data class CommonFeedbackSystemData(
|
||||
appendLine(getDisabledBundledPluginsForDialog())
|
||||
appendLine(CommonFeedbackBundle.message("dialog.feedback.system.info.panel.nonbundled.plugins"))
|
||||
appendLine(getNonBundledPluginsForDialog())
|
||||
appendLine(CommonFeedbackBundle.message("dialog.feedback.system.info.panel.remote.dev.host"))
|
||||
appendLine(isRemoteDevelopmentHost.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user