Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/io/flutter/actions/FlutterSdkAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import io.flutter.FlutterBundle;
import io.flutter.FlutterMessages;
import io.flutter.FlutterUtils;
import io.flutter.analytics.Analytics;
import io.flutter.analytics.AnalyticsConstants;
import io.flutter.analytics.AnalyticsData;
import io.flutter.bazel.Workspace;
import io.flutter.pub.PubRoot;
import io.flutter.pub.PubRoots;
Expand All @@ -32,19 +35,25 @@ 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(AnalyticsConstants.IN_BAZEL_CONTEXT, true);
Analytics.report(analyticsData);
return;
}
}

final FlutterSdk sdk = project != null ? FlutterSdk.getFlutterSdk(project) : null;
if (sdk == null) {
showMissingSdkDialog(project);
analyticsData.add(AnalyticsConstants.MISSING_SDK, true);
Analytics.report(analyticsData);
return;
}

Expand All @@ -60,6 +69,8 @@ public void actionPerformed(@NotNull AnActionEvent event) {
startCommand(project, sdk, sub, context);
}
}

Analytics.report(analyticsData);
}

public abstract void startCommand(@NotNull Project project,
Expand Down
8 changes: 8 additions & 0 deletions src/io/flutter/actions/ReloadFlutterApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import icons.FlutterIcons;
import io.flutter.FlutterBundle;
import io.flutter.FlutterConstants;
import io.flutter.analytics.Analytics;
import io.flutter.analytics.AnalyticsConstants;
import io.flutter.analytics.AnalyticsData;
import io.flutter.run.FlutterReloadManager;
import io.flutter.run.daemon.FlutterApp;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -42,9 +45,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(AnalyticsConstants.REQUIRES_RESTART, shouldRestart);

var reloadManager = FlutterReloadManager.getInstance(project);
if (reloadManager == null) return;
Expand All @@ -56,6 +62,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.
Expand Down
9 changes: 9 additions & 0 deletions src/io/flutter/actions/RestartFlutterApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import io.flutter.FlutterBundle;
import io.flutter.FlutterConstants;
import io.flutter.FlutterMessages;
import io.flutter.analytics.Analytics;
import io.flutter.analytics.AnalyticsConstants;
import io.flutter.analytics.AnalyticsData;
import io.flutter.bazel.WorkspaceCache;
import io.flutter.run.FlutterReloadManager;
import io.flutter.run.daemon.FlutterApp;
Expand Down Expand Up @@ -50,6 +53,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()) {
Expand All @@ -61,8 +66,12 @@ public void actionPerformed(@NotNull AnActionEvent e) {
NotificationType.INFORMATION);
Notifications.Bus.notify(notification, project);

analyticsData.add(AnalyticsConstants.GOOGLE3, true);

// We only want to show this notification once.
FlutterSettings.getInstance().setShowBazelHotRestartWarning(false);
}

Analytics.report(analyticsData);
}
}
160 changes: 160 additions & 0 deletions src/io/flutter/analytics/Analytics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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) = data.reportTo(reporter)
}

abstract class AnalyticsReporter {
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(type: String) {
val data = mutableMapOf<String, Any>()

init {
add(AnalyticsConstants.TYPE, type)
}

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 <T> add(key: DataValue<T>, value: T) = key.addTo(this, value)

internal operator fun set(key: String, value: Boolean) {
data[key] = value
}

internal operator fun set(key: String, value: Int) {
data[key] = value
}

internal operator fun set(key: String, value: String) {
data[key] = value
}

open fun reportTo(reporter: AnalyticsReporter) = reporter.process(this)
}

/**
* 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 <a href="https://plugins.jetbrains.com/docs/intellij/basic-action-system.html">IntelliJ Action System</a>
*/
class ActionData(private val id: String?, private val place: String) : AnalyticsData("action") {

init {
id?.let { add(AnalyticsConstants.ID, it) }
add(AnalyticsConstants.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)
}
}

/**
* Defines standard keys for analytics data properties.
*
* The properties are exposed as `@JvmField`s to be easily accessible as static
* fields from Java.
*/
object AnalyticsConstants {
/**
* Indicates if the project is a Google3 project.
*/
@JvmField
val GOOGLE3 = BooleanValue("google3")

/**
* The unique identifier for an action or event.
*/
@JvmField
val ID = StringValue("id")

/**
* Indicates if the project is in a Bazel context.
*/
@JvmField
val IN_BAZEL_CONTEXT = BooleanValue("inBazelContext")

/**
* Indicates if the Flutter SDK is missing.
*/
@JvmField
val MISSING_SDK = BooleanValue("missingSdk")

/**
* The UI location where an action was invoked, as provided by
* [com.intellij.openapi.actionSystem.PlaceProvider.getPlace] (for example, "MainMenu",
* "MainToolbar", "EditorPopup", "GoToAction", etc).
*/
@JvmField
val PLACE = StringValue("place")

/**
* Indicates if a restart is required for a hot reload request.
*/
@JvmField
val REQUIRES_RESTART = BooleanValue("requiresRestart")

/**
* The type of the analytics event (e.g., "action", ...).
*/
@JvmField
val TYPE = StringValue("type")
}


sealed class DataValue<T>(val name: String) {
abstract fun addTo(data: AnalyticsData, value: T);
}

class StringValue(name: String) : DataValue<String>(name) {
override fun addTo(data: AnalyticsData, value: String) {
data[name] = value
}
}

class IntValue(name: String) : DataValue<Int>(name) {
override fun addTo(data: AnalyticsData, value: Int) {
data[name] = value
}
}

class BooleanValue(name: String) : DataValue<Boolean>(name) {
override fun addTo(data: AnalyticsData, value: Boolean) {
data[name] = value
}
}