diff --git a/src/io/flutter/actions/DeviceSelectorAction.java b/src/io/flutter/actions/DeviceSelectorAction.java index 594fef85c..812eb6b31 100644 --- a/src/io/flutter/actions/DeviceSelectorAction.java +++ b/src/io/flutter/actions/DeviceSelectorAction.java @@ -38,10 +38,10 @@ import com.intellij.util.ModalityUiUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; import icons.FlutterIcons; import io.flutter.FlutterBundle; -import io.flutter.analytics.Analytics; -import io.flutter.analytics.AnalyticsData; import io.flutter.logging.PluginLogger; import io.flutter.run.FlutterDevice; import io.flutter.run.daemon.DeviceService; diff --git a/src/io/flutter/actions/DeviceSelectorRefresherAction.java b/src/io/flutter/actions/DeviceSelectorRefresherAction.java index 0f63c674c..df2079caa 100644 --- a/src/io/flutter/actions/DeviceSelectorRefresherAction.java +++ b/src/io/flutter/actions/DeviceSelectorRefresherAction.java @@ -9,8 +9,8 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.Project; -import io.flutter.analytics.Analytics; -import io.flutter.analytics.AnalyticsData; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; import io.flutter.run.daemon.DeviceService; import io.flutter.utils.FlutterModuleUtils; import org.jetbrains.annotations.NotNull; diff --git a/src/io/flutter/actions/FlutterGettingStartedAction.java b/src/io/flutter/actions/FlutterGettingStartedAction.java index b3aee7ff8..a3f34a064 100644 --- a/src/io/flutter/actions/FlutterGettingStartedAction.java +++ b/src/io/flutter/actions/FlutterGettingStartedAction.java @@ -8,16 +8,16 @@ import com.intellij.ide.browsers.BrowserLauncher; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.DumbAwareAction; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; import io.flutter.FlutterConstants; -import io.flutter.analytics.Analytics; -import io.flutter.analytics.AnalyticsData; import org.jetbrains.annotations.NotNull; public class FlutterGettingStartedAction extends DumbAwareAction { @Override public void actionPerformed(@NotNull final AnActionEvent e) { Analytics.report(AnalyticsData.forAction(this, e)); - + BrowserLauncher.getInstance().browse(FlutterConstants.URL_GETTING_STARTED_IDE, null); } } diff --git a/src/io/flutter/actions/FlutterSdkAction.java b/src/io/flutter/actions/FlutterSdkAction.java index 5babc1bc5..a361bf2af 100644 --- a/src/io/flutter/actions/FlutterSdkAction.java +++ b/src/io/flutter/actions/FlutterSdkAction.java @@ -10,12 +10,12 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; 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; diff --git a/src/io/flutter/actions/FlutterSubmitFeedback.java b/src/io/flutter/actions/FlutterSubmitFeedback.java index 2b2258674..51eea592d 100644 --- a/src/io/flutter/actions/FlutterSubmitFeedback.java +++ b/src/io/flutter/actions/FlutterSubmitFeedback.java @@ -8,8 +8,8 @@ import com.intellij.ide.browsers.BrowserLauncher; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.DumbAwareAction; -import io.flutter.analytics.Analytics; -import io.flutter.analytics.AnalyticsData; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; import org.jetbrains.annotations.NotNull; public class FlutterSubmitFeedback extends DumbAwareAction { diff --git a/src/io/flutter/actions/RefreshToolWindowAction.java b/src/io/flutter/actions/RefreshToolWindowAction.java index 6d7522f14..8b74c1ede 100644 --- a/src/io/flutter/actions/RefreshToolWindowAction.java +++ b/src/io/flutter/actions/RefreshToolWindowAction.java @@ -9,10 +9,10 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; import io.flutter.FlutterBundle; import io.flutter.FlutterUtils; -import io.flutter.analytics.Analytics; -import io.flutter.analytics.AnalyticsData; import org.jetbrains.annotations.NotNull; import java.util.Optional; diff --git a/src/io/flutter/actions/ReloadFlutterApp.java b/src/io/flutter/actions/ReloadFlutterApp.java index c3b3f47ff..9001eecce 100644 --- a/src/io/flutter/actions/ReloadFlutterApp.java +++ b/src/io/flutter/actions/ReloadFlutterApp.java @@ -9,12 +9,12 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; 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; diff --git a/src/io/flutter/actions/RestartFlutterApp.java b/src/io/flutter/actions/RestartFlutterApp.java index 1d258925b..d3e3c73b4 100644 --- a/src/io/flutter/actions/RestartFlutterApp.java +++ b/src/io/flutter/actions/RestartFlutterApp.java @@ -12,13 +12,13 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; +import com.jetbrains.lang.dart.analytics.Analytics; +import com.jetbrains.lang.dart.analytics.AnalyticsData; import icons.FlutterIcons; 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; diff --git a/src/io/flutter/analytics/Analytics.kt b/src/io/flutter/analytics/Analytics.kt index cb0103fdc..dd3f7fbc8 100644 --- a/src/io/flutter/analytics/Analytics.kt +++ b/src/io/flutter/analytics/Analytics.kt @@ -6,83 +6,7 @@ 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() - - 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 add(key: DataValue, 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 IntelliJ Action System - */ -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) - } -} +import com.jetbrains.lang.dart.analytics.BooleanValue /** * Defines standard keys for analytics data properties. @@ -97,12 +21,6 @@ object AnalyticsConstants { @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. */ @@ -115,46 +33,9 @@ object AnalyticsConstants { @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(val name: String) { - abstract fun addTo(data: AnalyticsData, value: T); -} - -class StringValue(name: String) : DataValue(name) { - override fun addTo(data: AnalyticsData, value: String) { - data[name] = value - } -} - -class IntValue(name: String) : DataValue(name) { - override fun addTo(data: AnalyticsData, value: Int) { - data[name] = value - } -} - -class BooleanValue(name: String) : DataValue(name) { - override fun addTo(data: AnalyticsData, value: Boolean) { - data[name] = value - } } diff --git a/src/io/flutter/analytics/UnifiedAnalytics.java b/src/io/flutter/analytics/UnifiedAnalytics.java deleted file mode 100644 index bb104cbc3..000000000 --- a/src/io/flutter/analytics/UnifiedAnalytics.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2024 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.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationType; -import com.intellij.notification.Notifications; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.project.Project; -import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; -import de.roderick.weberknecht.WebSocketException; -import io.flutter.dart.DtdUtils; -import io.flutter.logging.PluginLogger; -import io.flutter.sdk.FlutterSdkUtil; -import io.flutter.utils.OpenApiUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.CompletableFuture; - -/** - * Facilitates sending information to unified analytics. - */ -public class UnifiedAnalytics { - private static final @NotNull Logger LOG = PluginLogger.createLogger(UnifiedAnalytics.class); - - @Nullable Boolean enabled = null; - final @NotNull Project project; - final @NotNull DtdUtils dtdUtils; - final @NotNull FlutterSdkUtil flutterSdkUtil; - - public UnifiedAnalytics(@NotNull Project project) { - this.project = project; - this.dtdUtils = new DtdUtils(); - this.flutterSdkUtil = new FlutterSdkUtil(); - } - - public void manageConsent() { - try { - DartToolingDaemonService service = dtdUtils.readyDtdService(project).get(); - if (service != null) { - final JsonObject params = new JsonObject(); - params.addProperty("tool", getToolName()); - - Boolean shouldShowMessage = shouldShowMessage(service, params).get(); - if (Boolean.TRUE.equals(shouldShowMessage)) { - String message = getConsentMessage(service, params).get(); - if (message == null) { - throw new Exception("Unified analytics consent message was null"); - } - Boolean canSendAnalytics = showMessage(message).get(); - if (canSendAnalytics != null) { - setTelemetry(service, params, canSendAnalytics); - clientShowedMessage(service, params); - } - } - // No message needed. Check on whether we should track analytics. - this.enabled = telemetryEnabled(service, params).get(); - } - } - catch (Exception e) { - LOG.info(e); - } - } - - /** - * Sends analytics data to the unified analytics service. - * - * @param analyticsData The data object containing the details of the event to report. - * @return A {@link CompletableFuture} that completes with a {@link SendResult}. - * The {@code SendResult} will indicate whether the report was successfully - * sent and may contain a message with additional details on failure. - * If the analytics service is unavailable or an error occurs, the future - * will complete with a result where {@code success} is false. - */ - public @NotNull CompletableFuture sendReport(AnalyticsData analyticsData) { - try { - DartToolingDaemonService service = dtdUtils.readyDtdService(project).get(); - if (service == null) { - return SendResult.failed("DTD service is null"); - } - - JsonObject params = new JsonObject(); - params.addProperty("tool", getToolName()); - - // TODO (pq): convert analyticsData to params. - - // TODO (pq): hoist request constants into static fields - return makeUnifiedAnalyticsRequest("ideEvent", service, params).thenCompose(result -> { - if (result == null) { - return SendResult.failed("ideEvent result is null"); - } - - JsonPrimitive type = result.getAsJsonPrimitive("type"); - if (type == null) { - return SendResult.failed("ideEvent result type is null"); - } - - return SendResult.succeeded("Success".equals(type.getAsString())); - }); - } - catch (Exception e) { - LOG.info(e); - return SendResult.failed(e.getMessage()); - } - } - - private @NotNull CompletableFuture makeUnifiedAnalyticsRequest(String requestName, - @NotNull DartToolingDaemonService service, - @NotNull JsonObject params) { - CompletableFuture finalResult = new CompletableFuture<>(); - try { - service.sendRequest("UnifiedAnalytics." + requestName, params, false, object -> { - JsonObject result = object.getAsJsonObject("result"); - if (result == null) { - finalResult.completeExceptionally(new Exception(requestName + " JSON result is malformed: " + object)); - return; - } - finalResult.complete(result); - }); - } - catch (WebSocketException e) { - finalResult.completeExceptionally(e); - } - return finalResult; - } - - private @NotNull CompletableFuture telemetryEnabled(@NotNull DartToolingDaemonService service, @NotNull JsonObject params) { - return makeUnifiedAnalyticsRequest("telemetryEnabled", service, params).thenCompose(result -> { - assert result != null; - JsonPrimitive value = result.getAsJsonPrimitive("value"); - CompletableFuture innerResult = new CompletableFuture<>(); - - if (value == null) { - return CompletableFuture.failedFuture(new Exception("telemetryEnabled value is null")); - } - innerResult.complete(value.getAsBoolean()); - return innerResult; - }); - } - - private @NotNull CompletableFuture clientShowedMessage(@NotNull DartToolingDaemonService service, @NotNull JsonObject params) { - return makeUnifiedAnalyticsRequest("clientShowedMessage", service, params).thenCompose(result -> { - assert result != null; - JsonPrimitive type = result.getAsJsonPrimitive("type"); - CompletableFuture innerResult = new CompletableFuture<>(); - - if (type == null) { - return CompletableFuture.failedFuture(new Exception("clientShowedMessage type is null")); - } - innerResult.complete("Success".equals(type.getAsString())); - return innerResult; - }); - } - - private @NotNull CompletableFuture setTelemetry(@NotNull DartToolingDaemonService service, - @NotNull JsonObject params, - Boolean canSendAnalytics) { - params.addProperty("enable", canSendAnalytics); - return makeUnifiedAnalyticsRequest("setTelemetry", service, params).thenCompose(result -> { - assert result != null; - JsonPrimitive type = result.getAsJsonPrimitive("type"); - CompletableFuture innerResult = new CompletableFuture<>(); - - if (type == null) { - return CompletableFuture.failedFuture(new Exception("setTelemetry type is null")); - } - innerResult.complete("Success".equals(type.getAsString())); - return innerResult; - }); - } - - private CompletableFuture showMessage(@NotNull String message) { - CompletableFuture finalResult = new CompletableFuture<>(); - OpenApiUtils.safeInvokeLater(() -> { - final Notification notification = new Notification( - "Flutter Usage Statistics", // Analytics.GROUP_DISPLAY_ID, - "Welcome to Flutter!", - message, - NotificationType.INFORMATION); - //noinspection DialogTitleCapitalization - notification.addAction(new AnAction("Sounds good!") { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - notification.expire(); - finalResult.complete(true); - } - }); - //noinspection DialogTitleCapitalization - notification.addAction(new AnAction("No thanks") { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - notification.expire(); - finalResult.complete(false); - } - }); - Notifications.Bus.notify(notification, project); - System.out.println("Ran notification"); - }); - return finalResult; - } - - private String getToolName() throws Exception { - String ideValue = flutterSdkUtil.getFlutterHostEnvValue(); - return switch (flutterSdkUtil.getFlutterHostEnvValue()) { - case "IntelliJ-IDEA" -> "intellij-plugins"; - case "Android-Studio" -> "android-studio-plugins"; - default -> throw new Exception("Tool name cannot be found for IDE type: " + ideValue); - }; - } - - private @NotNull CompletableFuture shouldShowMessage(@NotNull DartToolingDaemonService service, @NotNull JsonObject params) { - return makeUnifiedAnalyticsRequest("shouldShowMessage", service, params).thenCompose(result -> { - assert result != null; - JsonPrimitive value = result.getAsJsonPrimitive("value"); - CompletableFuture innerResult = new CompletableFuture<>(); - - if (value == null) { - return CompletableFuture.failedFuture(new Exception("shouldShowMessage value is null")); - } - innerResult.complete(value.getAsBoolean()); - return innerResult; - }); - } - - private @NotNull CompletableFuture getConsentMessage(@NotNull DartToolingDaemonService service, @NotNull JsonObject params) { - return makeUnifiedAnalyticsRequest("getConsentMessage", service, params).thenCompose(result -> { - assert result != null; - JsonPrimitive value = result.getAsJsonPrimitive("value"); - CompletableFuture innerResult = new CompletableFuture<>(); - if (value == null) { - return CompletableFuture.failedFuture(new Exception("getConsentMessage value is null")); - } - innerResult.complete(value.getAsString()); - return innerResult; - }); - } -} - -/** - * Represents the result of an analytics sending operation. - *

- * This class encapsulates whether the operation was successful and includes an - * optional message, typically used for logging errors. - */ -class SendResult { - /** - * True if the analytics report was sent successfully, false otherwise. - */ - public final boolean success; - - /** - * An optional message providing more details about the result, particularly on failure. - */ - public final @Nullable String message; - - SendResult(boolean success, @Nullable String message) { - this.success = success; - this.message = message; - } - - CompletableFuture toCompletedFuture() { - return CompletableFuture.completedFuture(this); - } - - static CompletableFuture failed(@Nullable String message) { - return new SendResult(false, message).toCompletedFuture(); - } - - static CompletableFuture succeeded(boolean success) { - return new SendResult(success, null).toCompletedFuture(); - } -}