PY-77813: Report new project type broken in NPW.

FUS statistics consists of two parts:
1. Interpreter (i.e "venv" or "conda")
2. Project generator type ("Django" or "Flask")

`com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector.GENERATOR_FIELD` was a class without any limitation and `DirectoryProjectGenerator` instance was reported (i.e one for Django).

When migrated to NPW, we:
1. Dropped most old generator classes
2. Called this function providing `this::class` by accident, and it was `CoroutineScope`, so we finished with lots of `CoroutineScope` as generator type in FUS.

We must:
1. Provide old names for project types to preserve statistics.
2. Make it type-safe this time.

We also found that interpreter statistics is nullable for `PySdkCreator` which isn't true: SDK creation statistics is always not null.

So we:
* Introduce interface for project generators that reports "name for the statistics"
* Implement it both for DS and PyCharm by returning class name by default
* Overwrite it for several well-known generators to preserve statistics (use old named of now-deleted classes)
* Make interpreter statistics not null.


(cherry picked from commit bdfa73ba043d3584c6ba1871bca7a464a550bc21)

KT-CR-19191

GitOrigin-RevId: 53f874c18d67d33083cf8508a58be257b5e89ab7
This commit is contained in:
Ilya.Kazakevich
2024-12-03 01:05:02 +01:00
committed by intellij-monorepo-bot
parent fa7590530b
commit 5666495862
12 changed files with 127 additions and 41 deletions

View File

@@ -1,15 +1,22 @@
// 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.pycharm.community.ide.impl.newProjectWizard.impl.emptyProject
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.newProjectWizard.PyV3ProjectBaseGenerator
import com.jetbrains.python.PyBundle
import com.jetbrains.python.newProjectWizard.collector.PyProjectTypeValidationRule
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.Nls
import javax.swing.Icon
internal class PyV3EmptyProjectGenerator : PyV3ProjectBaseGenerator<PyV3EmptyProjectSettings>(
@Internal
class PyV3EmptyProjectGenerator : PyV3ProjectBaseGenerator<PyV3EmptyProjectSettings>(
PyV3EmptyProjectSettings(generateWelcomeScript = false), PyV3EmptyProjectUI, _newProjectName = "PythonProject") {
override fun getName(): @Nls String = PyBundle.message("pure.python.project")
override fun getLogo(): Icon = PythonPsiApiIcons.Python
override val projectTypeForStatistics: @NlsSafe String = PyProjectTypeValidationRule.EMPTY_PROJECT_TYPE_ID
}

View File

@@ -158,5 +158,7 @@
<orderEntry type="library" name="kotlinx-datetime-jvm" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.remote" />
<orderEntry type="module" module-name="intellij.platform.ide.ui" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="library" name="ap-validation" level="project" />
</component>
</module>

View File

@@ -510,6 +510,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.namespacePackages.PyNamespacePackagesStatisticsCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.codeInsight.codeVision.PyCodeVisionUsageCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector"/>
<statistics.validation.customValidationRule implementation="com.jetbrains.python.newProjectWizard.collector.PyProjectTypeValidationRule"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.run.runAnything.PyRunAnythingCollector"/>
<statistics.counterUsagesCollector implementationClass="com.jetbrains.python.debugger.statistics.PyDataViewerCollector"/>

View File

@@ -15,6 +15,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.NlsContexts.DialogMessage;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.DirectoryProjectGeneratorBase;
@@ -22,6 +23,7 @@ import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyPsiPackageUtil;
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo;
import com.jetbrains.python.newProjectWizard.collector.PyProjectTypeGenerator;
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector;
import com.jetbrains.python.packaging.PyPackage;
import com.jetbrains.python.packaging.PyPackageManager;
@@ -49,7 +51,8 @@ import java.util.function.Consumer;
* @deprecated Use {@link com.jetbrains.python.newProjectWizard}
*/
@Deprecated
public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> extends DirectoryProjectGeneratorBase<T> {
public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> extends DirectoryProjectGeneratorBase<T> implements
PyProjectTypeGenerator {
public static final PyNewProjectSettings NO_SETTINGS = new PyNewProjectSettings();
private static final Logger LOGGER = Logger.getInstance(PythonProjectGenerator.class);
@@ -72,6 +75,11 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> ext
this(false, null);
}
@Override
public final @NlsSafe @NotNull String getProjectTypeForStatistics() {
return getClass().getName();
}
/**
* @param allowRemoteProjectCreation if project of this type could be created remotely
* @param preferredInterpreter interpreter type to select by default
@@ -179,7 +187,7 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> ext
if (statisticsInfo instanceof InterpreterStatisticsInfo interpreterStatisticsInfo && settings.getSdk() != null) {
PythonNewProjectWizardCollector.logPythonNewProjectGenerated(interpreterStatisticsInfo,
PyStatisticToolsKt.getVersion(settings.getSdk()),
this.getClass(),
this,
Collections.emptyList());
}
}

View File

@@ -2,20 +2,20 @@
package com.jetbrains.python.newProjectWizard
import com.intellij.openapi.GitRepositoryInitializer
import com.intellij.openapi.application.EDT
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector.logPythonNewProjectGenerated
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.PySdkCreator
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.statistics.version
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
* Settings each Python project has: [sdkCreator] and [createGitRepository]
@@ -23,7 +23,7 @@ import kotlinx.coroutines.*
class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
lateinit var sdkCreator: PySdkCreator
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Result<Sdk> = coroutineScope {
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Result<Pair<Sdk, InterpreterStatisticsInfo>> = coroutineScope {
val project = module.project
if (createGitRepository) {
launch(CoroutineName("Generating git") + Dispatchers.IO) {
@@ -32,22 +32,14 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
}
}
}
val (sdk: Sdk, statistics: InterpreterStatisticsInfo?) = getSdkAndInterpreter(module).getOrElse { return@coroutineScope Result.failure(it) }
val (sdk: Sdk, interpreterStatistics: InterpreterStatisticsInfo) = getSdkAndInterpreter(module).getOrElse { return@coroutineScope Result.failure(it) }
sdk.setAssociationToModule(module)
module.pythonSdk = sdk
if (statistics != null) {
logPythonNewProjectGenerated(statistics,
sdk.version,
this::class.java,
emptyList())
}
return@coroutineScope Result.success(sdk)
return@coroutineScope Result.success(Pair(sdk, interpreterStatistics))
}
private suspend fun getSdkAndInterpreter(module: Module): Result<Pair<Sdk, InterpreterStatisticsInfo?>> = withContext(Dispatchers.EDT) {
val sdk: Sdk = sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module)).getOrElse { return@withContext Result.failure(it) }
return@withContext Result.success(Pair<Sdk, InterpreterStatisticsInfo?>(sdk, sdkCreator.createStatisticsInfo()))
}
private suspend fun getSdkAndInterpreter(module: Module): Result<Pair<Sdk, InterpreterStatisticsInfo>> =
sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module))
override fun toString(): String {

View File

@@ -14,17 +14,19 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.platform.ProjectGeneratorPeer
import com.jetbrains.python.Result
import com.jetbrains.python.newProjectWizard.collector.PyProjectTypeGenerator
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector.logPythonNewProjectGenerated
import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
import com.jetbrains.python.statistics.version
import com.jetbrains.python.util.ErrorSink
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import java.nio.file.Path
import kotlin.reflect.jvm.jvmName
/**
* Extend this class to register a new project generator.
@@ -41,15 +43,16 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
private val errorSink: ErrorSink = ShowingMessageErrorSync,
private val _newProjectName: @NlsSafe String? = null,
private val expandProjectAfterCreation: Boolean = !ApplicationManager.getApplication().isHeadlessEnvironment,
) : DirectoryProjectGenerator<PyV3BaseProjectSettings> {
) : DirectoryProjectGenerator<PyV3BaseProjectSettings>, PyProjectTypeGenerator {
private val baseSettings = PyV3BaseProjectSettings()
val newProjectName: @NlsSafe String get() = _newProjectName ?: "${name.replace(" ", "")}Project"
override val projectTypeForStatistics: @NlsSafe String = this::class.jvmName
override fun generateProject(project: Project, baseDir: VirtualFile, settings: PyV3BaseProjectSettings, module: Module) {
val coroutineScope = project.service<MyService>().coroutineScope
coroutineScope.launch {
val sdk = settings.generateAndGetSdk(module, baseDir).getOrElse {
val (sdk, interpreterStatistics) = settings.generateAndGetSdk(module, baseDir).getOrElse {
withContext(Dispatchers.EDT) {
errorSink.emit(it.localizedMessage) // Show error generation to user
}
@@ -58,6 +61,14 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
// Project view must be expanded (PY-75909) but it can't be unless it contains some files.
// Either base settings (which create venv) might generate some or type specific settings (like Django) may.
// So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
val pythonVersion = withContext(Dispatchers.IO) { sdk.version }
logPythonNewProjectGenerated(interpreterStatistics,
pythonVersion,
this@PyV3ProjectBaseGenerator,
emptyList())
ensureProjectViewExpanded(project)
typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure { errorSink.emit(it.localizedMessage) }
ensureProjectViewExpanded(project)

View File

@@ -0,0 +1,24 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.newProjectWizard.collector
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.annotations.ApiStatus
/**
* [com.intellij.platform.DirectoryProjectGenerator] for something python-specific.
* You must implement both [com.intellij.platform.DirectoryProjectGenerator] and this interface.
*
* This interface is a part of internal JetBrains infrastructure.
* Extend [com.jetbrains.python.newProjectWizard.PyV3ProjectBaseGenerator] instead.
*/
@ApiStatus.Internal
interface PyProjectTypeGenerator {
/**
* Project type for FUS, see [PythonNewProjectWizardCollector.GENERATOR_FIELD].
* Try not to change it ofter not to break statistics.
*
* This property is for JetBrains plugins only and will be ignored for other plugins.
* Do not overwrite it if you aren't JetBrains employee.
*/
val projectTypeForStatistics: @NlsSafe String
}

View File

@@ -0,0 +1,36 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.newProjectWizard.collector
import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
import com.intellij.internal.statistic.eventLog.validator.ValidationResultType
import com.intellij.internal.statistic.eventLog.validator.rules.EventContext
import com.intellij.internal.statistic.eventLog.validator.rules.impl.CustomValidationRule
import com.intellij.internal.statistic.utils.getPluginInfo
import com.jetbrains.python.newProjectWizard.collector.PyProjectTypeValidationRule.Companion.EMPTY_PROJECT_TYPE_ID
import org.jetbrains.annotations.ApiStatus.Internal
/**
* Ensures project type provided to [doValidate] is [PyProjectTypeGenerator.projectTypeForStatistics]
*/
@Internal
class PyProjectTypeValidationRule : CustomValidationRule() {
companion object {
/**
* [com.intellij.platform.DirectoryProjectGenerator] for default (empty, base) project type isn't registered in EP, hence hardcoded
*/
const val EMPTY_PROJECT_TYPE_ID = "com.intellij.pycharm.community.ide.impl.newProject.steps.PythonBaseProjectGenerator"
}
override fun getRuleId(): String = "python_new_project_type"
override fun doValidate(data: String, context: EventContext): ValidationResultType = validate(data)
}
@Internal
fun validate(data: String): ValidationResultType {
val valid = data == EMPTY_PROJECT_TYPE_ID || AbstractNewProjectStep.EP_NAME
.extensionList
.filterIsInstance<PyProjectTypeGenerator>()
.any { getPluginInfo(it::class.java).isDevelopedByJetBrains() && it.projectTypeForStatistics == data }
return if (valid) ValidationResultType.ACCEPTED else ValidationResultType.REJECTED
}

View File

@@ -7,6 +7,7 @@ import com.intellij.internal.statistic.eventLog.events.EventFields.createAdditio
import com.intellij.internal.statistic.eventLog.events.EventPair
import com.intellij.internal.statistic.eventLog.events.ObjectEventData
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.platform.DirectoryProjectGenerator
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.statistics.EXECUTION_TYPE
@@ -22,13 +23,13 @@ object PythonNewProjectWizardCollector : CounterUsagesCollector() {
return GROUP
}
private val GROUP = EventLogGroup("python.new.project.wizard", 9)
private val GROUP = EventLogGroup("python.new.project.wizard", 10)
const val PROJECT_GENERATED_EVENT_ID = "project.generated"
private val INHERIT_GLOBAL_SITE_PACKAGE_FIELD = EventFields.Boolean("inherit_global_site_package")
private val MAKE_AVAILABLE_TO_ALL_PROJECTS = EventFields.Boolean("make_available_to_all_projects")
private val PREVIOUSLY_CONFIGURED = EventFields.Boolean("previously_configured")
private val IS_WSL_CONTEXT = EventFields.Boolean("wsl_context")
private val GENERATOR_FIELD = EventFields.Class("generator")
private val GENERATOR_FIELD = EventFields.StringValidatedByCustomRule("generator", PyProjectTypeValidationRule::class.java)
private val DJANGO_ADMIN_FIELD = EventFields.Boolean("django_admin")
private val ADDITIONAL = createAdditionalDataField(GROUP.id, PROJECT_GENERATED_EVENT_ID)
@@ -50,12 +51,12 @@ object PythonNewProjectWizardCollector : CounterUsagesCollector() {
private val USE_EXISTING_VENV_FIX = GROUP.registerEvent("existing.venv")
@JvmStatic
fun logPythonNewProjectGenerated(
fun <T> logPythonNewProjectGenerated(
info: InterpreterStatisticsInfo,
pythonVersion: LanguageLevel,
generatorClass: Class<*>,
generator: T,
additionalData: List<EventPair<*>>,
) {
) where T : PyProjectTypeGenerator, T : DirectoryProjectGenerator<*> {
PROJECT_GENERATED_EVENT.log(
INTERPRETER_TYPE.with(info.type.value),
EXECUTION_TYPE.with(info.target.value),
@@ -64,7 +65,7 @@ object PythonNewProjectWizardCollector : CounterUsagesCollector() {
INHERIT_GLOBAL_SITE_PACKAGE_FIELD.with(info.globalSitePackage),
MAKE_AVAILABLE_TO_ALL_PROJECTS.with(info.makeAvailableToAllProjects),
PREVIOUSLY_CONFIGURED.with(info.previouslyConfigured),
GENERATOR_FIELD.with(generatorClass),
GENERATOR_FIELD.with(generator.projectTypeForStatistics),
IS_WSL_CONTEXT.with(info.isWSLContext),
ADDITIONAL.with(ObjectEventData(additionalData))
)

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.jetbrains.python.newProjectWizard.collector;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -2,7 +2,6 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
@@ -10,8 +9,5 @@ interface PySdkCreator {
/**
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Sdk>
@RequiresEdt
fun createStatisticsInfo(): InterpreterStatisticsInfo? = null
suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Pair<Sdk, InterpreterStatisticsInfo>>
}

View File

@@ -20,6 +20,7 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.bindText
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
@@ -158,12 +159,12 @@ class PythonAddNewEnvironmentPanel(val projectPathFlows: ProjectPathFlows, onlyA
}
else {
runBlockingCancellable { getSdk(moduleOrProject) }
}.getOrThrow()
}.getOrThrow().first
}
override suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
override suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Pair<Sdk, InterpreterStatisticsInfo>> {
model.navigator.saveLastState()
return when (selectedMode.get()) {
val sdk = when (selectedMode.get()) {
PROJECT_VENV -> {
val projectPath = projectPathFlows.projectPathWithDefault.first()
// todo just keep venv path, all the rest is in the model
@@ -171,11 +172,13 @@ class PythonAddNewEnvironmentPanel(val projectPathFlows: ProjectPathFlows, onlyA
}
BASE_CONDA -> model.selectCondaEnvironment(base = true)
CUSTOM -> custom.currentSdkManager.getOrCreateSdk(moduleOrProject)
}
}.getOrElse { return Result.failure(it) }
val statistics = withContext(Dispatchers.EDT) { createStatisticsInfo() }
return Result.success(Pair(sdk, statistics))
}
override fun createStatisticsInfo(): InterpreterStatisticsInfo = when (selectedMode.get()) {
@RequiresEdt
fun createStatisticsInfo(): InterpreterStatisticsInfo = when (selectedMode.get()) {
PROJECT_VENV -> InterpreterStatisticsInfo(InterpreterType.VIRTUALENV,
InterpreterTarget.LOCAL,
false,