From 5c0bc7d1773b9174c9d1fb256526f35a107f0f22 Mon Sep 17 00:00:00 2001 From: pq Date: Mon, 3 Nov 2025 09:08:18 -0800 Subject: [PATCH 1/2] [analytics] preliminary Analytics reporting API --- src/io/flutter/actions/FlutterAppAction.java | 4 + src/io/flutter/actions/FlutterSdkAction.java | 10 +++ src/io/flutter/actions/ReloadFlutterApp.java | 7 ++ src/io/flutter/actions/RestartFlutterApp.java | 8 ++ src/io/flutter/analytics/Analytics.kt | 76 +++++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 src/io/flutter/analytics/Analytics.kt diff --git a/src/io/flutter/actions/FlutterAppAction.java b/src/io/flutter/actions/FlutterAppAction.java index 574253da58..0607f2f689 100644 --- a/src/io/flutter/actions/FlutterAppAction.java +++ b/src/io/flutter/actions/FlutterAppAction.java @@ -98,4 +98,8 @@ public void update(@NotNull final AnActionEvent e) { public FlutterApp getApp() { return myApp; } + + public @NotNull String getId() { + return myActionId; + } } diff --git a/src/io/flutter/actions/FlutterSdkAction.java b/src/io/flutter/actions/FlutterSdkAction.java index 4707b790c1..db64abf3df 100644 --- a/src/io/flutter/actions/FlutterSdkAction.java +++ b/src/io/flutter/actions/FlutterSdkAction.java @@ -13,6 +13,8 @@ import io.flutter.FlutterBundle; import io.flutter.FlutterMessages; import io.flutter.FlutterUtils; +import io.flutter.analytics.Analytics; +import io.flutter.analytics.AnalyticsData; import io.flutter.bazel.Workspace; import io.flutter.pub.PubRoot; import io.flutter.pub.PubRoots; @@ -32,12 +34,16 @@ public abstract class FlutterSdkAction extends DumbAwareAction { public void actionPerformed(@NotNull AnActionEvent event) { final Project project = DumbAwareAction.getEventProject(event); + AnalyticsData analyticsData = AnalyticsData.forAction(this, event); + if (enableActionInBazelContext()) { // See if the Bazel workspace exists for this project. final Workspace workspace = FlutterModuleUtils.getFlutterBazelWorkspace(project); if (workspace != null) { FileDocumentManager.getInstance().saveAllDocuments(); startCommandInBazelContext(project, workspace, event); + analyticsData.add("inBazelContext", true); + Analytics.report(analyticsData); return; } } @@ -45,6 +51,8 @@ public void actionPerformed(@NotNull AnActionEvent event) { final FlutterSdk sdk = project != null ? FlutterSdk.getFlutterSdk(project) : null; if (sdk == null) { showMissingSdkDialog(project); + analyticsData.add("missingSdk", true); + Analytics.report(analyticsData); return; } @@ -60,6 +68,8 @@ public void actionPerformed(@NotNull AnActionEvent event) { startCommand(project, sdk, sub, context); } } + + Analytics.report(analyticsData); } public abstract void startCommand(@NotNull Project project, diff --git a/src/io/flutter/actions/ReloadFlutterApp.java b/src/io/flutter/actions/ReloadFlutterApp.java index 6e2ed80376..6ec637b919 100644 --- a/src/io/flutter/actions/ReloadFlutterApp.java +++ b/src/io/flutter/actions/ReloadFlutterApp.java @@ -12,6 +12,8 @@ import icons.FlutterIcons; import io.flutter.FlutterBundle; import io.flutter.FlutterConstants; +import io.flutter.analytics.Analytics; +import io.flutter.analytics.AnalyticsData; import io.flutter.run.FlutterReloadManager; import io.flutter.run.daemon.FlutterApp; import org.jetbrains.annotations.NotNull; @@ -42,9 +44,12 @@ public void actionPerformed(@NotNull AnActionEvent e) { return; } + var analyticsData = AnalyticsData.forAction(this, e); + // If the shift key is held down, perform a restart. We check to see if we're being invoked from the // 'GoToAction' dialog. If so, the modifiers are for the command that opened the go-to action dialog. final boolean shouldRestart = (e.getModifiers() & InputEvent.SHIFT_MASK) != 0 && !"GoToAction".equals(e.getPlace()); + analyticsData.add("requiresRestart", shouldRestart); var reloadManager = FlutterReloadManager.getInstance(project); if (reloadManager == null) return; @@ -56,6 +61,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { // Else perform a hot reload. reloadManager.saveAllAndReload(getApp(), FlutterConstants.RELOAD_REASON_MANUAL); } + + Analytics.report(analyticsData); } // Override to disable the hot reload action when running flutter web apps. diff --git a/src/io/flutter/actions/RestartFlutterApp.java b/src/io/flutter/actions/RestartFlutterApp.java index fd68e0e6da..10aa036438 100644 --- a/src/io/flutter/actions/RestartFlutterApp.java +++ b/src/io/flutter/actions/RestartFlutterApp.java @@ -16,6 +16,8 @@ import io.flutter.FlutterBundle; import io.flutter.FlutterConstants; import io.flutter.FlutterMessages; +import io.flutter.analytics.Analytics; +import io.flutter.analytics.AnalyticsData; import io.flutter.bazel.WorkspaceCache; import io.flutter.run.FlutterReloadManager; import io.flutter.run.daemon.FlutterApp; @@ -50,6 +52,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { reloadManager.saveAllAndRestart(getApp(), FlutterConstants.RELOAD_REASON_MANUAL); } + var analyticsData = AnalyticsData.forAction(this, e); + if (WorkspaceCache.getInstance(project).isBazel() && FlutterSettings.getInstance().isShowBazelHotRestartWarning() && !FlutterSettings.getInstance().isEnableBazelHotRestart()) { @@ -61,8 +65,12 @@ public void actionPerformed(@NotNull AnActionEvent e) { NotificationType.INFORMATION); Notifications.Bus.notify(notification, project); + analyticsData.add("google3", true); + // We only want to show this notification once. FlutterSettings.getInstance().setShowBazelHotRestartWarning(false); } + + Analytics.report(analyticsData); } } diff --git a/src/io/flutter/analytics/Analytics.kt b/src/io/flutter/analytics/Analytics.kt new file mode 100644 index 0000000000..6ac74bd240 --- /dev/null +++ b/src/io/flutter/analytics/Analytics.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2025 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +package io.flutter.analytics + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import io.flutter.actions.FlutterAppAction + +object Analytics { + private val reporter = NoOpReporter + + @JvmStatic + fun report(data: AnalyticsData) = reporter.report(data) +} + +abstract class AnalyticsReporter { + + fun report(data: AnalyticsData) = data.reportTo(this) + + internal abstract fun process(data: AnalyticsData) +} + +internal object PrintingReporter : AnalyticsReporter() { + override fun process(data: AnalyticsData) = println(data.data) + +} + +internal object NoOpReporter : AnalyticsReporter() { + override fun process(data: AnalyticsData) = Unit +} + +abstract class AnalyticsData { + val data = mutableMapOf() + + companion object { + @JvmStatic + fun forAction(action: AnAction, event: AnActionEvent): ActionData = ActionData( + event.actionManager.getId(action) + // `FlutterAppAction`s aren't registered so ask them directly. + ?: (action as? FlutterAppAction)?.id, + event.place + ) + } + + fun add(key: String, value: Boolean) { + data[key] = value + } + + fun add(key: String, value: Int) { + data[key] = value + } + + fun add(key: String, value: String) { + data[key] = value + } + + open fun reportTo(reporter: AnalyticsReporter) = reporter.process(this) +} + +class ActionData(private val id: String?, private val place: String) : AnalyticsData() { + + init { + id?.let { add("id", it) } + add("place", place) + } + + override fun reportTo(reporter: AnalyticsReporter) { + // We only report if we have an id for the event. + if (id == null) return + super.reportTo(reporter) + } +} From 62ed91e754b0c2a805ce9b63501c52b5cbb42bbb Mon Sep 17 00:00:00 2001 From: pq Date: Mon, 3 Nov 2025 17:34:47 -0800 Subject: [PATCH 2/2] review feedback --- src/io/flutter/analytics/Analytics.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/io/flutter/analytics/Analytics.kt b/src/io/flutter/analytics/Analytics.kt index 6ac74bd240..7f9c37fb19 100644 --- a/src/io/flutter/analytics/Analytics.kt +++ b/src/io/flutter/analytics/Analytics.kt @@ -33,9 +33,13 @@ internal object NoOpReporter : AnalyticsReporter() { override fun process(data: AnalyticsData) = Unit } -abstract class AnalyticsData { +abstract class AnalyticsData(type: String) { val data = mutableMapOf() + init { + add("type", type) + } + companion object { @JvmStatic fun forAction(action: AnAction, event: AnActionEvent): ActionData = ActionData( @@ -61,7 +65,14 @@ abstract class AnalyticsData { open fun reportTo(reporter: AnalyticsReporter) = reporter.process(this) } -class ActionData(private val id: String?, private val place: String) : AnalyticsData() { +/** + * Data describing an IntelliJ [com.intellij.openapi.actionSystem.AnAction] for analytics reporting. + * + * @param id The unique identifier of the action, typically defined in `plugin.xml`. + * @param place The UI location where the action was invoked (e.g., "MainMenu", "Toolbar"). + * @see IntelliJ Action System + */ +class ActionData(private val id: String?, private val place: String) : AnalyticsData("action") { init { id?.let { add("id", it) }