IJPL-175691 FUS: Detect and log presence of top public plugins in vscode

IJ-CR-154139

(cherry picked from commit 66832c04b70897ba41c125ae8bbd4fd0f97d1c0b)

GitOrigin-RevId: be433850b16169c0fed170e431a5144fb8078e8b
This commit is contained in:
Dmitry Pogrebnoy
2025-01-31 20:21:46 +01:00
committed by intellij-monorepo-bot
parent 148d3fce6e
commit d302ab9c34
3 changed files with 213 additions and 2 deletions

View File

@@ -18,7 +18,7 @@ private const val WINDSURF_ID = ".windsurf"
private const val ECLIPSE_ID = ".eclipse"
internal class EditorsCollector : ApplicationUsagesCollector() {
private val EDITORS_GROUP: EventLogGroup = EventLogGroup("editors", 2)
private val EDITORS_GROUP: EventLogGroup = EventLogGroup("editors", 3)
override fun getGroup(): EventLogGroup = EDITORS_GROUP
@@ -35,6 +35,15 @@ internal class EditorsCollector : ApplicationUsagesCollector() {
EventFields.String("config", CONFIGS)
)
private val IS_VSCODE_USED_RECENTLY: EventId1<Boolean> = EDITORS_GROUP.registerEvent(
"vscode.used.recently",
EventFields.Boolean("is_vscode_used_recently"))
private val VS_CODE_EXTENSION_INSTALLED: EventId1<List<String>> = EDITORS_GROUP.registerEvent(
"vscode.extension.installed",
EventFields.StringList("extension_ids", emptyList())
)
override suspend fun getMetricsAsync(): Set<MetricEvent> {
val homeDir = System.getProperty("user.home")
return withContext(Dispatchers.IO) {
@@ -47,7 +56,16 @@ internal class EditorsCollector : ApplicationUsagesCollector() {
add(CONFIG_EXISTS.metric(VIMRC_ID))
}
if (Files.isDirectory(Paths.get(homeDir, ".vscode"))) {
val vsCodeCollectionDataProvider = VSCodeCollectionDataProvider()
if (vsCodeCollectionDataProvider.isVSCodeDetected()) {
val isVSCodeUsedRecently = vsCodeCollectionDataProvider.isVSCodeUsedRecently()
isVSCodeUsedRecently?.let {
add(IS_VSCODE_USED_RECENTLY.metric(it))
}
if (vsCodeCollectionDataProvider.isVSCodePluginsProcessingPossible()) {
add(VS_CODE_EXTENSION_INSTALLED.metric(vsCodeCollectionDataProvider.getVSCodePluginsIds()))
}
add(CONFIG_EXISTS.metric(VSCODE_ID))
}

View File

@@ -0,0 +1,15 @@
// 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.internal.statistic.collectors.fus.environment
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
internal abstract class ExternalEditorCollectionDataProvider {
protected val homeDirectory: Path? = try {
Paths.get(System.getProperty("user.home"))
}
catch (_: InvalidPathException) {
null
}
}

View File

@@ -0,0 +1,178 @@
// 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.internal.statistic.collectors.fus.environment
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.SystemInfo.isMac
import com.intellij.openapi.util.SystemInfo.isWindows
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.io.path.*
private const val VSCODE_PLUGINS_IDENTIFICATION_TAG = "identifier"
private const val VSCODE_PLUGINS_ID_TAG = "id"
private val logger = logger<VSCodeCollectionDataProvider>()
internal class VSCodeCollectionDataProvider : ExternalEditorCollectionDataProvider() {
private val vsCodeHomePath: Path? = when {
isMac -> homeDirectory?.resolve(Paths.get("Library", "Application Support", "Code"))
isWindows -> getWindowsVSCodeHomePath()
else -> homeDirectory?.resolve(Paths.get(".config", "Code"))
}
private val pluginsDirectoryPath = homeDirectory?.resolve(Paths.get(".vscode", "extensions"))
private val pluginsConfigPath = pluginsDirectoryPath?.resolve("extensions.json")
private val databasePath = vsCodeHomePath?.resolve(Paths.get("User", "globalStorage", "state.vscdb"))
init {
logger.debug { "VSCode home path: $vsCodeHomePath" }
logger.debug { "VSCode plugins directory path: $pluginsDirectoryPath" }
logger.debug { "VSCode plugins config path: $pluginsConfigPath" }
logger.debug { "VSCode database path: $databasePath" }
}
private val maxTimeSinceLastModificationToBeRecent = Duration.ofHours(30 * 24) // 30 days
fun isVSCodeDetected(): Boolean {
if (homeDirectory == null) {
logger.debug { "VSCode is not detected - home directory is null" }
return false
}
val vsCodeConfigDir = homeDirectory.resolve(".vscode")
return try {
val isVsCodeConfigDirValid = Files.isDirectory(vsCodeConfigDir)
logger.debug { "Is $vsCodeConfigDir a valid directory: $isVsCodeConfigDirValid" }
isVsCodeConfigDirValid
}
catch (e: SecurityException) {
logger.debug(e)
false
}
}
fun isVSCodeUsedRecently(): Boolean? {
return try {
if (databasePath?.exists() == true) {
val time = Files.getLastModifiedTime(databasePath)
val isVSCodeUsedRecently = time.toInstant() > Instant.now() - maxTimeSinceLastModificationToBeRecent
logger.debug { "Is VSCode used recently: $isVSCodeUsedRecently" }
isVSCodeUsedRecently
}
logger.debug { "VSCode is not used recently - database path doesn't exist" }
null
}
catch (e: IOException) {
logger.debug(e)
null
}
catch (e: SecurityException) {
logger.debug(e)
null
}
}
fun isVSCodePluginsProcessingPossible(): Boolean {
return try {
val isVSCodePluginDirExists = pluginsDirectoryPath?.exists() == true
val isVSCodeConfigDirValid = pluginsDirectoryPath?.isDirectory() == true
val isVSCodeConfigFilePathExists = pluginsConfigPath?.exists() == true
val isVSCodeConfigFilePathValid = pluginsConfigPath?.isRegularFile() == true
val isVSCodeConfigFilePathReadable = pluginsConfigPath?.isReadable() == true
logger.debug {
"isVSCodePluginDirExists = $isVSCodePluginDirExists\n" +
"isVSCodeConfigDirValid = $isVSCodeConfigDirValid\n" +
"isVSCodeConfigFilePathExists = $isVSCodeConfigFilePathExists\n" +
"isVSCodeConfigFilePathValid = $isVSCodeConfigFilePathValid\n" +
"isVSCodeConfigFilePathReadable = $isVSCodeConfigFilePathReadable"
}
isVSCodePluginDirExists && isVSCodeConfigDirValid &&
isVSCodeConfigFilePathExists && isVSCodeConfigFilePathValid &&
isVSCodeConfigFilePathReadable
}
catch (e: SecurityException) {
logger.debug(e)
false
}
}
fun getVSCodePluginsIds(): List<String> {
val pluginsConfigData = pluginsConfigPath?.readText() ?: return emptyList()
val parsedJson = try {
Json.parseToJsonElement(pluginsConfigData)
}
catch (e: SerializationException) {
logger.debug(e)
return emptyList()
}
val pluginIdsList = mutableListOf<String>()
try {
for (pluginElement in parsedJson.jsonArray) {
val pluginInfoObject = pluginElement.jsonObject
pluginInfoObject[VSCODE_PLUGINS_IDENTIFICATION_TAG]?.jsonObject?.get(VSCODE_PLUGINS_ID_TAG)?.let {
pluginIdsList.add(it.jsonPrimitive.content)
}
}
}
catch (e: IllegalArgumentException) {
logger.debug(e)
return pluginIdsList
}
logger.debug { "Detected VSCode plugins: ${pluginIdsList.joinToString(",")}}" }
return pluginIdsList
}
private fun getWindowsVSCodeHomePath(): Path? {
return try {
val appDataValue = Paths.get(getWindowsEnvVariableValue("APPDATA"), "Code")
logger.debug { "Detected APPDATA env var value: $appDataValue" }
appDataValue
}
catch (e: InvalidPathException) {
logger.debug(e)
null
}
}
/**
* Function to get one Windows env var.
*
* Use without %%. Example: get("APPDATA")
*
* @return env var value or "null" string if not found
*/
private fun getWindowsEnvVariableValue(variable: String): String {
val envMap = try {
System.getenv()
}
catch (e: SecurityException) {
logger.debug(e)
return "null"
}
val varValue = envMap[variable] ?: envMap[variable.uppercase(Locale.getDefault())].toString()
logger.debug { "Detected system env var value - $variable: $varValue" }
return varValue
}
}