feat(intellij): add anonymous usage event IntelliJInitFailed. (#365)

* feat(intellij): add anonymous usage event plugin activated.

* fix(intellij): post event IntelliJinitFailed only when failed instead of PluginActivated.
release-0.0
Zhiming Ma 2023-08-21 13:51:41 +08:00 committed by GitHub
parent b1ad936033
commit bdda8a534f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 224 additions and 95 deletions

View File

@ -1,11 +1,11 @@
# Tabby Plugin for IntelliJ Platform
## Requirements
- Tabby plugin is compatiple with all IntelliJ Platform IDEs build 2022.2.5 or later versions, including Idea, Pycharm, Android Studio and so on.
- Tabby plugin requires [Node.js](https://nodejs.org) 16.0+ installed and added into `PATH` enviroment variable.
- Tabby plugin works with all IntelliJ Platform IDEs that have build 2022.2.5 or later versions, such as Idea, PyCharm, Android Studio, and more.
- Tabby plugin requires [Node.js](https://nodejs.org) 16.0+ to be installed and added into the `PATH` environment variable.
## Installation
Tabby plugin could be installed from the IntelliJ Platform [plugin marketplace](https://plugins.jetbrains.com/plugin/22379-tabby).
You can install Tabby plugin from the IntelliJ Platform [plugin marketplace](https://plugins.jetbrains.com/plugin/22379-tabby).
## Development and Build
Please clone this directory and open it with IntelliJ Idea.
To develop and build Tabby plugin, please clone this directory and open it with IntelliJ Idea.

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,6 @@ import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.Key
@ -26,8 +25,8 @@ import java.io.OutputStreamWriter
class Agent : ProcessAdapter() {
private val logger = Logger.getInstance(Agent::class.java)
private val gson = Gson()
private val process: KillableProcessHandler
private val streamWriter: OutputStreamWriter
private lateinit var process: KillableProcessHandler
private lateinit var streamWriter: OutputStreamWriter
enum class Status {
NOT_INITIALIZED,
@ -41,7 +40,9 @@ class Agent : ProcessAdapter() {
private val authRequiredEventFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authRequiredEvent = authRequiredEventFlow.asSharedFlow()
init {
open class AgentException(message: String) : Exception(message)
fun open() {
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
@ -49,7 +50,7 @@ class Agent : ProcessAdapter() {
logger.info("Node bin path: ${node.absolutePath}")
} else {
logger.error("Node bin not found")
throw Error("Node bin not found")
throw AgentException("Node bin not found")
}
val script =
@ -59,7 +60,7 @@ class Agent : ProcessAdapter() {
logger.info("Node script path: ${script.absolutePath}")
} else {
logger.error("Node script not found")
throw Error("Node script not found")
throw AgentException("Node script not found")
}
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
@ -97,15 +98,12 @@ class Agent : ProcessAdapter() {
)
}
suspend fun initialize(config: Config): Boolean {
val appInfo = ApplicationInfo.getInstance().fullApplicationName
val pluginId = "com.tabbyml.intellij-tabby"
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version
suspend fun initialize(config: Config, client: String): Boolean {
return request(
"initialize", listOf(
mapOf(
"config" to config,
"client" to "$appInfo $pluginId $pluginVersion",
"client" to client,
)
)
)
@ -142,8 +140,11 @@ class Agent : ProcessAdapter() {
@SerializedName("choice_index") val choiceIndex: Int,
) {
enum class EventType {
@SerializedName("view") VIEW,
@SerializedName("select") SELECT,
@SerializedName("view")
VIEW,
@SerializedName("select")
SELECT,
}
}

View File

@ -1,28 +1,30 @@
package com.tabbyml.intellijtabby.agent
import com.intellij.ide.BrowserUtil
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.lang.Language
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.tabbyml.intellijtabby.actions.OpenAuthPage
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger
import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -36,12 +38,23 @@ class AgentService : Disposable {
init {
val settings = service<ApplicationSettingsState>()
val anonymousUsageLogger = service<AnonymousUsageLogger>()
scope.launch {
val appInfo = ApplicationInfo.getInstance().fullApplicationName
val pluginId = "com.tabbyml.intellij-tabby"
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version
val client = "$appInfo $pluginId $pluginVersion"
try {
agent.initialize(createAgentConfig(settings.data))
agent.open()
agent.initialize(createAgentConfig(settings.data), client)
logger.info("Agent init done.")
} catch (e: Error) {
} catch (e: Exception) {
logger.error("Agent init failed: $e")
anonymousUsageLogger.event("IntelliJInitFailed", mapOf(
"client" to client,
"error" to e.stackTraceToString()
))
}
}

View File

@ -0,0 +1,115 @@
package com.tabbyml.intellijtabby.usage
import com.google.gson.Gson
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import kotlin.io.path.Path
@Service
class AnonymousUsageLogger {
private val logger = Logger.getInstance(AnonymousUsageLogger::class.java)
private val gson = Gson()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private lateinit var anonymousId: String
private val disabled: Boolean
get() {
return service<ApplicationSettingsState>().isAnonymousUsageTrackingDisabled
}
private val initialized = MutableStateFlow(false)
init {
scope.launch {
try {
val home = System.getProperty("user.home")
logger.info("User home: $home")
val datafile = Path(home).resolve(".tabby/agent/data.json").toFile()
var data: Map<*, *>? = null
try {
val dataJson = datafile.inputStream().bufferedReader().use { it.readText() }
data = gson.fromJson(dataJson, Map::class.java)
} catch (e: Exception) {
logger.error("Failed to load anonymous ID: ${e.message}")
}
if (data?.get("anonymousId") != null) {
anonymousId = data["anonymousId"].toString()
logger.info("Saved anonymous ID: $anonymousId")
} else {
anonymousId = UUID.randomUUID().toString()
val newData = data?.toMutableMap() ?: mutableMapOf()
newData["anonymousId"] = anonymousId
val newDataJson = gson.toJson(newData)
datafile.writeText(newDataJson)
logger.info("Create new anonymous ID: $anonymousId")
}
} catch (e: Exception) {
logger.error("Failed when init anonymous ID: ${e.message}")
anonymousId = UUID.randomUUID().toString()
} finally {
initialized.value = true
}
}
}
data class UsageRequest(
val distinctId: String,
val event: String,
val properties: Map<String, String>,
)
suspend fun event(event: String, properties: Map<String, String>) {
initialized.first { it }
if (disabled) {
return
}
val request = UsageRequest(
distinctId = anonymousId,
event = event,
properties = properties,
)
val requestString = gson.toJson(request)
withContext(scope.coroutineContext) {
try {
val connection = URL(ENDPOINT).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Accept", "application/json")
connection.doInput = true
connection.doOutput = true
val outputStreamWriter = OutputStreamWriter(connection.outputStream)
outputStreamWriter.write(requestString)
outputStreamWriter.flush()
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
logger.info("Usage event sent successfully.")
} else {
logger.error("Usage event failed to send: $responseCode")
}
connection.disconnect()
} catch (e: Exception) {
logger.error("Usage event failed to send: ${e.message}")
}
}
}
companion object {
const val ENDPOINT = "https://app.tabbyml.com/api/usage"
}
}