diff --git a/python/src/com/jetbrains/python/packaging/management/PyPackageManagerEditorBanner.kt b/python/src/com/jetbrains/python/packaging/management/PyPackageManagerEditorBanner.kt index 78e4fd09c02a..d3c8eb648260 100644 --- a/python/src/com/jetbrains/python/packaging/management/PyPackageManagerEditorBanner.kt +++ b/python/src/com/jetbrains/python/packaging/management/PyPackageManagerEditorBanner.kt @@ -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) } diff --git a/python/src/com/jetbrains/python/packaging/statistics/PythonPackagesToolwindowStatisticsCollector.kt b/python/src/com/jetbrains/python/packaging/statistics/PythonPackagesToolwindowStatisticsCollector.kt index b2e6e74dc421..b2e7085169cd 100644 --- a/python/src/com/jetbrains/python/packaging/statistics/PythonPackagesToolwindowStatisticsCollector.kt +++ b/python/src/com/jetbrains/python/packaging/statistics/PythonPackagesToolwindowStatisticsCollector.kt @@ -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") -} \ No newline at end of file + 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 = GROUP.registerEvent( + "install.dialog.opened", + EventFields.Enum("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 = GROUP.registerEvent( + "anchor.toggled", + EventFields.Enum("from"), + EventFields.Enum("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 = 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, +} diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt index 9680a0beb386..39984c941df6 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt @@ -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 { diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt index aa7f86333887..159ed72b2a69 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt @@ -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) } diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyInstallPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyInstallPackageAction.kt index 491c7bac288f..f3291c6c26ee 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyInstallPackageAction.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyInstallPackageAction.kt @@ -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().currentSdk == null) return + PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.SHORTCUT) PyInstallPackageDialog(project).show() } diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyTogglePackagingToolWindowAnchorAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyTogglePackagingToolWindowAnchorAction.kt index 9f1f7cb0df26..20154c0d88b4 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyTogglePackagingToolWindowAnchorAction.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PyTogglePackagingToolWindowAnchorAction.kt @@ -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 diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/marker/PyDependencyGroupInlayHintsProvider.kt b/python/src/com/jetbrains/python/packaging/toolwindow/marker/PyDependencyGroupInlayHintsProvider.kt index 55fd56f90ea9..a196ec58f39e 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/marker/PyDependencyGroupInlayHintsProvider.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/marker/PyDependencyGroupInlayHintsProvider.kt @@ -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().initForSdk(moduleSdk) } withContext(Dispatchers.EDT) { + PythonPackagesToolwindowStatisticsCollector.installDialogOpenedEvent.log(PyInstallDialogSource.INLAY_HINT) PyInstallPackageDialog(project).show(preselectModuleName = preselectName, preselectGroupName = groupName) } } diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/packages/tree/PyPackagesTree.kt b/python/src/com/jetbrains/python/packaging/toolwindow/packages/tree/PyPackagesTree.kt index 3663ef55bf46..31665849f80c 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/packages/tree/PyPackagesTree.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/packages/tree/PyPackagesTree.kt @@ -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) { diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyInstallPackageDialog.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyInstallPackageDialog.kt index 5a6988a31bc9..6a7afcd7f4bc 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyInstallPackageDialog.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyInstallPackageDialog.kt @@ -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())