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
parent
b1ad936033
commit
bdda8a534f
|
|
@ -1,11 +1,11 @@
|
||||||
# Tabby Plugin for IntelliJ Platform
|
# Tabby Plugin for IntelliJ Platform
|
||||||
|
|
||||||
## Requirements
|
## 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 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+ installed and added into `PATH` enviroment variable.
|
- Tabby plugin requires [Node.js](https://nodejs.org) 16.0+ to be installed and added into the `PATH` environment variable.
|
||||||
|
|
||||||
## Installation
|
## 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
|
## 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
|
|
@ -10,7 +10,6 @@ import com.intellij.execution.process.ProcessAdapter
|
||||||
import com.intellij.execution.process.ProcessEvent
|
import com.intellij.execution.process.ProcessEvent
|
||||||
import com.intellij.execution.process.ProcessOutputTypes
|
import com.intellij.execution.process.ProcessOutputTypes
|
||||||
import com.intellij.ide.plugins.PluginManagerCore
|
import com.intellij.ide.plugins.PluginManagerCore
|
||||||
import com.intellij.openapi.application.ApplicationInfo
|
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.extensions.PluginId
|
import com.intellij.openapi.extensions.PluginId
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
|
|
@ -26,8 +25,8 @@ import java.io.OutputStreamWriter
|
||||||
class Agent : ProcessAdapter() {
|
class Agent : ProcessAdapter() {
|
||||||
private val logger = Logger.getInstance(Agent::class.java)
|
private val logger = Logger.getInstance(Agent::class.java)
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val process: KillableProcessHandler
|
private lateinit var process: KillableProcessHandler
|
||||||
private val streamWriter: OutputStreamWriter
|
private lateinit var streamWriter: OutputStreamWriter
|
||||||
|
|
||||||
enum class Status {
|
enum class Status {
|
||||||
NOT_INITIALIZED,
|
NOT_INITIALIZED,
|
||||||
|
|
@ -41,7 +40,9 @@ class Agent : ProcessAdapter() {
|
||||||
private val authRequiredEventFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
private val authRequiredEventFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
val authRequiredEvent = authRequiredEventFlow.asSharedFlow()
|
val authRequiredEvent = authRequiredEventFlow.asSharedFlow()
|
||||||
|
|
||||||
init {
|
open class AgentException(message: String) : Exception(message)
|
||||||
|
|
||||||
|
fun open() {
|
||||||
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
|
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
|
||||||
|
|
||||||
val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
|
val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
|
||||||
|
|
@ -49,7 +50,7 @@ class Agent : ProcessAdapter() {
|
||||||
logger.info("Node bin path: ${node.absolutePath}")
|
logger.info("Node bin path: ${node.absolutePath}")
|
||||||
} else {
|
} else {
|
||||||
logger.error("Node bin not found")
|
logger.error("Node bin not found")
|
||||||
throw Error("Node bin not found")
|
throw AgentException("Node bin not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
val script =
|
val script =
|
||||||
|
|
@ -59,7 +60,7 @@ class Agent : ProcessAdapter() {
|
||||||
logger.info("Node script path: ${script.absolutePath}")
|
logger.info("Node script path: ${script.absolutePath}")
|
||||||
} else {
|
} else {
|
||||||
logger.error("Node script not found")
|
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)
|
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
|
||||||
|
|
@ -97,15 +98,12 @@ class Agent : ProcessAdapter() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initialize(config: Config): Boolean {
|
suspend fun initialize(config: Config, client: String): Boolean {
|
||||||
val appInfo = ApplicationInfo.getInstance().fullApplicationName
|
|
||||||
val pluginId = "com.tabbyml.intellij-tabby"
|
|
||||||
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version
|
|
||||||
return request(
|
return request(
|
||||||
"initialize", listOf(
|
"initialize", listOf(
|
||||||
mapOf(
|
mapOf(
|
||||||
"config" to config,
|
"config" to config,
|
||||||
"client" to "$appInfo $pluginId $pluginVersion",
|
"client" to client,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -142,8 +140,11 @@ class Agent : ProcessAdapter() {
|
||||||
@SerializedName("choice_index") val choiceIndex: Int,
|
@SerializedName("choice_index") val choiceIndex: Int,
|
||||||
) {
|
) {
|
||||||
enum class EventType {
|
enum class EventType {
|
||||||
@SerializedName("view") VIEW,
|
@SerializedName("view")
|
||||||
@SerializedName("select") SELECT,
|
VIEW,
|
||||||
|
|
||||||
|
@SerializedName("select")
|
||||||
|
SELECT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,30 @@
|
||||||
package com.tabbyml.intellijtabby.agent
|
package com.tabbyml.intellijtabby.agent
|
||||||
|
|
||||||
import com.intellij.ide.BrowserUtil
|
import com.intellij.ide.BrowserUtil
|
||||||
|
import com.intellij.ide.plugins.PluginManagerCore
|
||||||
import com.intellij.lang.Language
|
import com.intellij.lang.Language
|
||||||
import com.intellij.notification.Notification
|
import com.intellij.notification.Notification
|
||||||
import com.intellij.notification.NotificationType
|
import com.intellij.notification.NotificationType
|
||||||
import com.intellij.notification.Notifications
|
import com.intellij.notification.Notifications
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
import com.intellij.openapi.actionSystem.ActionManager
|
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
|
import com.intellij.openapi.application.ApplicationInfo
|
||||||
import com.intellij.openapi.application.ReadAction
|
import com.intellij.openapi.application.ReadAction
|
||||||
import com.intellij.openapi.application.invokeLater
|
import com.intellij.openapi.application.invokeLater
|
||||||
import com.intellij.openapi.components.Service
|
import com.intellij.openapi.components.Service
|
||||||
import com.intellij.openapi.components.service
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.extensions.PluginId
|
||||||
import com.intellij.openapi.progress.ProgressIndicator
|
import com.intellij.openapi.progress.ProgressIndicator
|
||||||
import com.intellij.psi.PsiDocumentManager
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
import com.tabbyml.intellijtabby.actions.OpenAuthPage
|
import com.tabbyml.intellijtabby.actions.OpenAuthPage
|
||||||
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
||||||
|
import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger
|
||||||
import io.ktor.util.*
|
import io.ktor.util.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -36,12 +38,23 @@ class AgentService : Disposable {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val settings = service<ApplicationSettingsState>()
|
val settings = service<ApplicationSettingsState>()
|
||||||
|
val anonymousUsageLogger = service<AnonymousUsageLogger>()
|
||||||
scope.launch {
|
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 {
|
try {
|
||||||
agent.initialize(createAgentConfig(settings.data))
|
agent.open()
|
||||||
|
agent.initialize(createAgentConfig(settings.data), client)
|
||||||
logger.info("Agent init done.")
|
logger.info("Agent init done.")
|
||||||
} catch (e: Error) {
|
} catch (e: Exception) {
|
||||||
logger.error("Agent init failed: $e")
|
logger.error("Agent init failed: $e")
|
||||||
|
anonymousUsageLogger.event("IntelliJInitFailed", mapOf(
|
||||||
|
"client" to client,
|
||||||
|
"error" to e.stackTraceToString()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue