[kotlin] Send months since first Kotlin and IDEA usage in BuildProcessSatisfaction survey

^KTIJ-31263 fixed

GitOrigin-RevId: e10ad3fe2648b3982db8c0fa87ec07b61e342352
This commit is contained in:
Frederik Haselmeier
2024-09-17 23:33:57 +02:00
committed by intellij-monorepo-bot
parent ef89768305
commit 71e4767fed
4 changed files with 58 additions and 9 deletions

View File

@@ -22,5 +22,7 @@ build.process.info.gradle.version=Gradle version:
build.process.info.kotlin.version=Kotlin version:
build.process.info.groovy.build.file.count=Number of Gradle Groovy build files:
build.process.info.kts.build.file.count=Number of Gradle Kts build files:
build.process.info.months.of.kotlin.usage=Months of Kotlin usage:
build.process.info.months.of.idea.usage=Months of IntelliJ IDEA usage:
action.org.jetbrains.kotlin.onboarding.gradle.ShowBuildProcessSatisfactionDialogAction.text=Show Kotlin Gradle Build Process Satisfaction Survey

View File

@@ -19,9 +19,12 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.idea.configuration.getGradleKotlinVersion
import org.jetbrains.kotlin.onboarding.KotlinNewUserTracker
import org.jetbrains.plugins.gradle.service.GradleInstallationManager
import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleUtil
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import kotlin.io.path.div
@Serializable
@@ -30,6 +33,8 @@ internal data class BuildProcessSatisfactionDialogData(
@NlsSafe val kotlinVersion: String,
val groovyBuildFileCount: Int,
val ktsBuildFileCount: Int,
val monthsOfIdeaUsage: Int,
val monthsOfKotlinUsage: Int,
val commonData: CommonFeedbackSystemData
) : SystemDataJsonSerializable {
override fun serializeToJson(json: Json): JsonElement = json.encodeToJsonElement(this)
@@ -63,6 +68,10 @@ internal class BuildProcessSatisfactionDialog(
return project.modules.mapNotNull { it.getGradleKotlinVersion() }.distinct()
}
private fun LocalDate.monthsSinceDate(): Int {
return ChronoUnit.MONTHS.between(this, LocalDate.now()).toInt()
}
private fun collectData(): BuildProcessSatisfactionDialogData {
val allExternalModulePaths = project.modules.mapNotNullTo(mutableSetOf()) {
ExternalSystemApiUtil.getExternalProjectPath(it)?.toNioPathOrNull()
@@ -77,12 +86,17 @@ internal class BuildProcessSatisfactionDialog(
val gradleVersion = getGradleVersion()?.version ?: "UNKNOWN"
val kotlinVersion = getKotlinVersions().maxOrNull() ?: "UNKNOWN"
val monthsOfIdeaUsage = KotlinNewUserTracker.getInstance().getInstallationDate()?.monthsSinceDate() ?: 0
val monthsOfKotlinUsage = KotlinNewUserTracker.getInstance().getFirstKotlinUsageDate()?.monthsSinceDate() ?: 0
return BuildProcessSatisfactionDialogData(
gradleVersion,
kotlinVersion,
groovyCount,
ktsCount,
CommonFeedbackSystemData.getCurrentData()
monthsOfIdeaUsage,
monthsOfKotlinUsage,
CommonFeedbackSystemData.getCurrentData(),
)
}
@@ -109,6 +123,12 @@ internal class BuildProcessSatisfactionDialog(
row(GradleFeedbackBundle.message("build.process.info.kts.build.file.count")) {
label(mySystemInfoData.ktsBuildFileCount.toString())
}
row(GradleFeedbackBundle.message("build.process.info.months.of.kotlin.usage")) {
label(mySystemInfoData.monthsOfKotlinUsage.toString())
}
row(GradleFeedbackBundle.message("build.process.info.months.of.idea.usage")) {
label(mySystemInfoData.monthsOfIdeaUsage.toString())
}
}
}
override val myTitle: String = GradleFeedbackBundle.message("dialog.build.process.gradle.satisfaction.top.title")

View File

@@ -8,11 +8,9 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.kotlin.idea.KotlinFileType
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.*
class KotlinNewUserTrackerState : BaseState() {
// Unix time seconds
@@ -54,7 +52,11 @@ class KotlinNewUserTracker : PersistentStateComponent<KotlinNewUserTrackerState>
}.getOrNull()
}
internal fun getInstallationDate(): LocalDate? {
/**
* Returns the date on which the user installed IDEA, or null if it could not be determined.
*/
@ApiStatus.Internal
fun getInstallationDate(): LocalDate? {
val installationId = getInstallationId() ?: return null
val dateSubstring = installationId.take(6).takeIf { it.length == 6 } ?: return null
val day = dateSubstring.substring(0..1).toIntOrNull() ?: return null
@@ -84,6 +86,18 @@ class KotlinNewUserTracker : PersistentStateComponent<KotlinNewUserTrackerState>
currentState = state
}
/**
* Returns the date on which the user was first detected to have used Kotlin.
* Returns null if an error occurred, or the user has not used Kotlin before.
*
* Note: This data only started being tracked in 2023, so that is the minimum date that exists and can be returned from here.
*/
@ApiStatus.Internal
fun getFirstKotlinUsageDate(): LocalDate? {
if (state.newKtUserSince == 0L) return null
return Instant.ofEpochSecond(state.newKtUserSince).atOffset(ZoneOffset.UTC).toLocalDate()
}
internal fun isNewKtUser(): Boolean {
if (state.newKtUserSince == 0L) return false
val newUserStart = Instant.ofEpochSecond(state.newKtUserSince)

View File

@@ -11,18 +11,23 @@ import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.*
class KotlinNewUserTrackerTest {
private val now = Instant.now()
private val today = LocalDate.now()
private fun Long.isRecentEpochTimestamp(): Boolean {
return Duration.between(Instant.ofEpochSecond(this), Instant.now()) < Duration.ofSeconds(30)
}
private fun LocalDate.daysSinceDate(): Int {
return ChronoUnit.DAYS.between(this, today).toInt()
}
private fun createInstance(
installationDate: LocalDate? = LocalDate.now().minusDays(NEW_IDEA_USER_DURATION.toDays() + 1)
): KotlinNewUserTracker {
@@ -43,6 +48,7 @@ class KotlinNewUserTrackerTest {
assertTrue(instance.state.firstKtFileOpened == 0L)
assertTrue(instance.state.newKtUserSince == 0L)
assertTrue(instance.state.firstKtFileOpened == 0L)
assertNull(instance.getFirstKotlinUsageDate())
}
@Test
@@ -53,6 +59,9 @@ class KotlinNewUserTrackerTest {
assertTrue(instance.state.lastKtFileOpened.isRecentEpochTimestamp())
assertTrue(instance.isNewKtUser())
assertFalse(instance.shouldShowNewUserDialog())
assertNotNull(instance.getFirstKotlinUsageDate())
// We are very careful and check <= 1 here in case the test changes over at that time, which is almost never going to happen
assertTrue(instance.getFirstKotlinUsageDate()!!.daysSinceDate() <= 1)
}
@Test
@@ -63,6 +72,7 @@ class KotlinNewUserTrackerTest {
assertTrue(instance.state.lastKtFileOpened.isRecentEpochTimestamp())
assertTrue(instance.isNewKtUser())
assertFalse(instance.shouldShowNewUserDialog())
assertTrue(instance.getFirstKotlinUsageDate()!!.daysSinceDate() <= 1)
}
@Test
@@ -84,6 +94,7 @@ class KotlinNewUserTrackerTest {
assertTrue(instance.isNewKtUser())
// User has not passed the new user period
assertFalse(instance.shouldShowNewUserDialog())
assertTrue(instance.getFirstKotlinUsageDate()!!.daysSinceDate() <= 1)
}
@Test
@@ -91,6 +102,8 @@ class KotlinNewUserTrackerTest {
val instance = createInstance()
instance.state.newKtUserSince = (now - NEW_USER_SURVEY_DELAY + Duration.ofHours(1)).epochSecond
assertFalse(instance.shouldShowNewUserDialog())
val newUserDays = NEW_USER_SURVEY_DELAY.toDays()
assertTrue(instance.getFirstKotlinUsageDate()!!.daysSinceDate() in (newUserDays - 1..newUserDays + 1))
}
@Test