PY-91367 [PPTW] Add usage statistics tracking for key interactions in the Install Package dialog and tool window.

Original MR: IJ-MR-216488


Merge-request: IJ-MR-216726
Merged-by: Timur Malanin <timur.malanin@jetbrains.com>

GitOrigin-RevId: e2db1ab8633b2d96397698720fb56b8550e2ddd0
This commit is contained in:
TimurMalanin
2026-08-03 14:40:17 +00:00
committed by intellij-monorepo-bot
parent 06bab38258
commit 29ba1b4ff8
9 changed files with 94 additions and 13 deletions
@@ -22,6 +22,7 @@ import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.components.TwoSideComponent
import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.requirements.RequirementsFileType
import org.jetbrains.annotations.ApiStatus
import java.awt.BorderLayout
@@ -116,6 +117,7 @@ internal class PyPackageManagerEditorBanner : EditorNotificationProvider, DumbAw
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
PythonPackagesToolwindowStatisticsCollector.navigateFromDependencyFileEvent.log()
ToolWindowManager.getInstance(project).getToolWindow(PACKAGING_TOOL_WINDOW_ID)?.activate(null)
}
@@ -1,17 +1,71 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.statistics
import com.intellij.ide.actions.ToolWindowMoveAction
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.EventId
import com.intellij.internal.statistic.eventLog.events.EventId1
import com.intellij.internal.statistic.eventLog.events.EventId2
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
object PythonPackagesToolwindowStatisticsCollector : CounterUsagesCollector() {
override fun getGroup() = GROUP
override fun getGroup(): EventLogGroup = GROUP
private val GROUP = EventLogGroup("python.packages.toolwindow", 1)
// Tool-window open/close and anchor changes are already covered by the platform "toolwindow" group
// (ToolWindowEventLogGroup): `shown`/`activated`/`hidden` events with `id="Python Packages"` and
// an `Anchor` `Location` field. This collector only tracks PPTW-specific interactions the
// platform does not see.
private val GROUP = EventLogGroup("python.packages.toolwindow", 2)
val installPackageEvent = GROUP.registerEvent("installed")
val uninstallPackageEvent = GROUP.registerEvent("uninstalled")
val requestDetailsEvent = GROUP.registerEvent("details.requested")
val repositoriesChangedEvent = GROUP.registerEvent("repositories.changed")
}
internal val installPackageEvent: EventId = GROUP.registerEvent("installed")
internal val uninstallPackageEvent: EventId = GROUP.registerEvent("uninstalled")
internal val requestDetailsEvent: EventId = GROUP.registerEvent("details.requested")
internal val repositoriesChangedEvent: EventId = GROUP.registerEvent("repositories.changed")
// Install-dialog entry points: keyboard shortcut / PPTW header install button / gutter or context
// action on an InstallablePackage tree node / dependency-group inlay hint.
internal val installDialogOpenedEvent: EventId1<PyInstallDialogSource> = GROUP.registerEvent(
"install.dialog.opened",
EventFields.Enum<PyInstallDialogSource>("source"),
)
// Not covered by the platform "toolwindow" group: we need to attribute anchor changes
// specifically to the PPTW toggle action (right/bottom button) with from/to, so users of the
// dashboard can compare "user dragged" vs "user clicked our action" flows.
internal val anchorToggledEvent: EventId2<ToolWindowMoveAction.Anchor, ToolWindowMoveAction.Anchor> = GROUP.registerEvent(
"anchor.toggled",
EventFields.Enum<ToolWindowMoveAction.Anchor>("from"),
EventFields.Enum<ToolWindowMoveAction.Anchor>("to"),
)
// DialogMode is internal; keep the collector free of that dependency by using a String allow-list
// that mirrors the DialogMode enum names.
internal val installDialogInstallEvent: EventId2<String?, Boolean> = GROUP.registerEvent(
"install.dialog.installed",
EventFields.String("mode", listOf("SEARCH", "DIRECT_INSTALL", "COMMAND")),
EventFields.Boolean("editable"),
)
// Editor-banner navigation from a dependency file to the PPTW.
internal val navigateFromDependencyFileEvent: EventId = GROUP.registerEvent("navigate.from.dependency.file")
}
/**
* Entry point that opened the [com.jetbrains.python.packaging.toolwindow.ui.PyInstallPackageDialog].
* Kept top-level (not nested in [PythonPackagesToolwindowStatisticsCollector]) so its FQN stays
* short in the generated FUS events scheme.
*/
internal enum class PyInstallDialogSource {
/** Keyboard shortcut / Find Action invocation of `PyInstallPackageAction`. */
SHORTCUT,
/** Install button in the PPTW header search bar. */
HEADER,
/** Click on the install icon of an `InstallablePackage` node in the tree. */
LIST_ICON,
/** Context-menu `InstallPackageAction` on a selected `InstallablePackage`. */
INSTALLABLE_ACTION,
/** `+ Add package` inlay hint inside a `pyproject.toml` dependency group. */
INLAY_HINT,
}
@@ -30,6 +30,8 @@ import com.intellij.ui.dsl.gridLayout.UnscaledGapsY
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.statistics.PyInstallDialogSource
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.actions.PyTogglePackagingToolWindowAnchorAction
import com.jetbrains.python.packaging.toolwindow.details.PyPackageInfoPanel
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
@@ -207,6 +209,7 @@ internal class PyPackagingToolWindowPanel(private val project: Project) : Simple
PyPackageIcons.AddPackage,
) {
override fun actionPerformed(e: AnActionEvent) {
PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.HEADER)
PyInstallPackageDialog(project).show(packageSearchController.text.trim().takeIf { it.isNotEmpty() })
}
}.apply {
@@ -4,6 +4,8 @@ package com.jetbrains.python.packaging.toolwindow.actions
import com.intellij.openapi.actionSystem.AnActionEvent
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.pyRequirement
import com.jetbrains.python.packaging.statistics.PyInstallDialogSource
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.ui.PyInstallPackageDialog
@@ -27,6 +29,7 @@ internal class InstallPackageAction : ModifyPackagesActionBase() {
return
}
val pkg = e.selectedPackage as? InstallablePackage ?: return
PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.INSTALLABLE_ACTION)
PyInstallPackageDialog(project).show(pkg.name)
}
@@ -5,6 +5,8 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.jetbrains.python.packaging.statistics.PyInstallDialogSource
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.ui.PyInstallPackageDialog
@@ -24,6 +26,7 @@ internal class PyInstallPackageAction : DumbAwareAction() {
// when no SDK is available, but invoking it through a keymap binding bypasses that gate in
// some contexts (Find Action / global shortcut), so re-check before showing the dialog.
if (project.service<PyPackagingToolWindowService>().currentSdk == null) return
PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.SHORTCUT)
PyInstallPackageDialog(project).show()
}
@@ -10,6 +10,7 @@ import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowAnchor
import com.intellij.openapi.wm.ToolWindowManager
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
internal class PyTogglePackagingToolWindowAnchorAction : DumbAwareAction() {
@@ -33,13 +34,17 @@ internal class PyTogglePackagingToolWindowAnchorAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
val toolWindow = findToolWindow(e) ?: return
val target = if (toolWindow.anchor == ToolWindowAnchor.RIGHT) {
ToolWindowMoveAction.Anchor.BottomLeft
// The action only swaps between the two corners its two icons point to, so encoding "from" as
// whichever of those two matches the current basic anchor is exact — no need to reach into
// `WindowInfo` (internal API) to distinguish split states we never produce ourselves.
val (from, to) = if (toolWindow.anchor == ToolWindowAnchor.RIGHT) {
ToolWindowMoveAction.Anchor.RightBottom to ToolWindowMoveAction.Anchor.BottomLeft
}
else {
ToolWindowMoveAction.Anchor.RightBottom
ToolWindowMoveAction.Anchor.BottomLeft to ToolWindowMoveAction.Anchor.RightBottom
}
target.applyTo(toolWindow)
to.applyTo(toolWindow)
PythonPackagesToolwindowStatisticsCollector.anchorToggledEvent.log(from, to)
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
@@ -36,6 +36,8 @@ import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.management.isDependencyGroupSupported
import com.jetbrains.python.packaging.statistics.PyInstallDialogSource
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.ui.PyInstallPackageDialog
import com.jetbrains.python.sdk.PythonSdkUtil
import org.toml.lang.psi.TomlKeySegment
@@ -120,6 +122,7 @@ internal class PyDependencyGroupInlayHintsProvider : InlayHintsProvider<NoSettin
project.service<PyPackagingToolWindowService>().initForSdk(moduleSdk)
}
withContext(Dispatchers.EDT) {
PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.INLAY_HINT)
PyInstallPackageDialog(project).show(preselectModuleName = preselectName, preselectGroupName = groupName)
}
}
@@ -37,6 +37,8 @@ import com.jetbrains.python.packaging.toolwindow.model.UndeclaredPackagesGroup
import com.jetbrains.python.packaging.toolwindow.packages.tree.renderers.PyPackageTreeCellRenderer
import com.jetbrains.python.packaging.toolwindow.packages.tree.renderers.asInstalledPackageOrNull
import com.jetbrains.python.packaging.toolwindow.packages.tree.renderers.trailingIconTooltip
import com.jetbrains.python.packaging.statistics.PyInstallDialogSource
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.ui.PyInstallPackageDialog
import com.jetbrains.python.packaging.toolwindow.ui.showChangeVersionPopup
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
@@ -377,7 +379,8 @@ internal class PyPackagesTree(
}
private fun showInstallDialog(pkg: InstallablePackage) {
PyInstallPackageDialog(project).show(pkg.name)
PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.LIST_ICON)
PyInstallPackageDialog(project).show(initialSearchText = pkg.name)
}
private fun updatePackageToLatest(pkg: InstalledPackage) {
@@ -38,6 +38,7 @@ import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.sdk.findFirstPythonSdk
import com.jetbrains.python.sdk.findModuleForSdk
@@ -553,6 +554,10 @@ internal class PyInstallPackageDialog(private val project: Project) : BigPopupUI
}
private fun performInstall() {
PythonPackagesToolwindowStatisticsCollector.installDialogInstallEvent.log(
currentMode.name,
versionPanel.editableCheckbox.isSelected,
)
when (currentMode) {
DialogMode.DIRECT_INSTALL -> performDirectInstall()
DialogMode.COMMAND -> performCommandExecution(mySearchField.text.trim())