From e7ca900c4b52d42759122f08a719d82605131666 Mon Sep 17 00:00:00 2001 From: WarningImHack3r <43064022+WarningImHack3r@users.noreply.github.com> Date: Sat, 18 May 2024 20:01:01 +0200 Subject: [PATCH] Improvements - Fix too much notifications for Vue with JS - Cache config - Better logs for RequestSender - Port crash reporter improvements from NUD, fixing crash - Upgrade dependencies --- CHANGELOG.md | 6 + build.gradle.kts | 10 +- gradle/libs.versions.toml | 6 +- .../backend/http/RequestSender.kt | 14 +- .../backend/sources/Source.kt | 14 +- .../backend/sources/impl/VueSource.kt | 14 +- .../errorsubmitter/ErrorThrowingAction.kt | 3 +- .../GitHubErrorReportSubmitter.kt | 133 ++++++++++++------ 8 files changed, 139 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b615e8..a3398fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ - Consequently, the plugin now depends on [Svelte](https://plugins.jetbrains.com/plugin/12375-svelte) & [Vue](https://plugins.jetbrains.com/plugin/9442-vue-js) extensions and only works on WebStorm or IntelliJ IDEA Ultimate +- Improve crash reporter to include more relevant information + +### Fixed + +- Fix a potential crash with 2024.1+ IDEs +- Fix JS users with Vue getting notified too often about the unavailability of the JS option ## [0.7.7] - 2024-03-29 diff --git a/build.gradle.kts b/build.gradle.kts index 8ee8b8f..082cfb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,10 +49,12 @@ changelog { } // Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration -koverReport { - defaults { - xml { - onCheck = true +kover { + reports { + total { + xml { + onCheck = true + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 687ab20..49f30b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -kotlin = "1.9.23" +kotlin = "1.9.24" changelog = "2.2.0" gradleIntelliJPlugin = "1.17.3" -qodana = "2024.1.3" -kover = "0.7.6" +qodana = "2024.1.5" +kover = "0.8.0" [plugins] changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/http/RequestSender.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/http/RequestSender.kt index d4d3c24..923c0bf 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/http/RequestSender.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/http/RequestSender.kt @@ -33,14 +33,24 @@ object RequestSender { log.debug( "Request method: ${request.method()}, headers: ${ request.headers().map() - }, body: ${body?.take(100)}${if ((body?.length ?: 0) > 100) "..." else ""}" + }, body:${ + if (body != null) { + "\n" + } else "" + }${body?.take(100)}${if ((body?.length ?: 0) > 100) "..." else ""}" ) HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) .build() .send(request, HttpResponse.BodyHandlers.ofString()).let { response -> return Response(response.statusCode(), response.headers().map(), response.body()).also { - log.debug("Request to $url returned ${it.statusCode} (${it.body.length} bytes): ${it.body.take(100)}${if (it.body.length > 100) "..." else ""}") + log.debug( + "Request to $url returned ${it.statusCode} (${it.body.length} bytes):${ + if (it.body.isNotEmpty()) { + "\n" + } else "" + }${it.body.take(100)}${if (it.body.length > 100) "..." else ""}" + ) } } } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt index 4adbc54..19644c4 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt @@ -24,6 +24,7 @@ import java.nio.file.NoSuchFileException abstract class Source(val project: Project, private val serializer: KSerializer) { private val log = logger>() abstract var framework: String + private var config: C? = null protected val domain: String get() = URI(getLocalConfig().`$schema`).let { uri -> "${uri.scheme}://${uri.host}".also { @@ -34,15 +35,22 @@ abstract class Source(val project: Project, private val serializer: // Utility methods protected fun getLocalConfig(): C { val file = "components.json" - return FileManager(project).getFileContentsAtPath(file)?.let { + return config?.also { + log.debug("Returning cached config") + } ?: FileManager(project).getFileContentsAtPath(file)?.let { log.debug("Parsing config from $file") try { - Json.decodeFromString(serializer, it).also { config -> - log.debug("Parsed config: ${config.javaClass.name}") + Json.decodeFromString(serializer, it).also { + log.debug("Parsed config") } } catch (e: Exception) { throw UnparseableConfigException(project, "Unable to parse $file", e) } + }?.also { + if (config == null) { + log.debug("Caching config") + config = it + } } ?: throw NoSuchFileException("$file not found") } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt index aafb129..519e886 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt @@ -21,6 +21,7 @@ import java.nio.file.NoSuchFileException class VueSource(project: Project) : Source(project, VueConfig.serializer()) { companion object { private val log = logger() + private var jsVueNotified = false } override var framework = "Vue" @@ -74,11 +75,14 @@ class VueSource(project: Project) : Source(project, VueConfig.seriali override fun adaptFileToConfig(file: PsiFile) { val config = getLocalConfig() if (!config.typescript) { - NotificationManager(project).sendNotification( - "TypeScript option for Vue", - "You have TypeScript disabled in your shadcn/ui config. This feature is not supported yet. Please install/update your components with the CLI for now.", - NotificationType.WARNING - ) + if (!jsVueNotified) { + NotificationManager(project).sendNotification( + "TypeScript option for Vue", + "You have TypeScript disabled in your shadcn/ui config. This feature is not supported yet. Please install/update your components with the CLI for now.", + NotificationType.WARNING + ) + jsVueNotified = true + } // TODO: detype Vue file } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/ErrorThrowingAction.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/ErrorThrowingAction.kt index dfb36e4..73d06ea 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/ErrorThrowingAction.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/ErrorThrowingAction.kt @@ -1,6 +1,7 @@ package com.github.warningimhack3r.intellijshadcnplugin.errorsubmitter import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.UpdateInBackground import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.DumbAwareAction @@ -13,7 +14,7 @@ import com.intellij.openapi.project.DumbAwareAction * #com.github.warningimhack3r.intellijshadcnplugin.errorreport.ErrorThrowingAction * ``` */ -class ErrorThrowingAction : DumbAwareAction() { +class ErrorThrowingAction : DumbAwareAction(), UpdateInBackground { companion object { private val LOG = logger() } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/GitHubErrorReportSubmitter.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/GitHubErrorReportSubmitter.kt index 0b0538f..ab2785f 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/GitHubErrorReportSubmitter.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/errorsubmitter/GitHubErrorReportSubmitter.kt @@ -22,13 +22,14 @@ import java.nio.charset.StandardCharsets // https://github.com/SonarSource/sonarlint-intellij/blob/master/src/main/java/org/sonarlint/intellij/errorsubmitter/BlameSonarSource.java class GitHubErrorReportSubmitter : ErrorReportSubmitter() { companion object { + private const val REPO_URL = "https://github.com/WarningImHack3r/intellij-shadcn-plugin" private const val MAX_URL_LENGTH = 2083 private const val BUG_LOGS_KEY = "bug-logs" private const val TRIMMED_STACKTRACE_MARKER = "\n\n" - private const val WORM_UNICODE = "\uD83D\uDC1B" + private const val UNICODE_WORM = "\uD83D\uDC1B" } - override fun getReportActionText() = "$WORM_UNICODE Open an Issue on GitHub" + override fun getReportActionText() = "$UNICODE_WORM Open an Issue on GitHub" override fun submit( events: Array, @@ -37,6 +38,7 @@ class GitHubErrorReportSubmitter : ErrorReportSubmitter() { consumer: Consumer ): Boolean { return try { + // Base data val event = if (events.isNotEmpty()) events.first() else null val stackTrace = event?.throwableText ?: "" @@ -48,13 +50,32 @@ class GitHubErrorReportSubmitter : ErrorReportSubmitter() { DataManager.getInstance().getDataContext(parentComponent) ) ?: getLastFocusedOrOpenedProject() - BrowserUtil.browse( - buildAbbreviatedUrl(mapOf( + // Computed data + var causedByLastIndex = -1 + val splitStackTrace = stackTrace.split("\n") + splitStackTrace.reversed().forEachIndexed { index, s -> + if (s.lowercase().startsWith("caused by")) { + causedByLastIndex = splitStackTrace.size - index + return@forEachIndexed + } + } + + // Build URL and content + BrowserUtil.browse(buildAbbreviatedUrl( + mapOf( "title" to "[crash] $simpleErrorMessage", "bug-explanation" to (additionalInfo ?: ""), - BUG_LOGS_KEY to stackTrace.split("\n").filter { - !it.trim().startsWith("at java.desktop/") - && !it.trim().startsWith("at java.base/") + BUG_LOGS_KEY to splitStackTrace.filterIndexed { index, s -> + if (index == 0) return@filterIndexed true + val line = s.trim() + if (causedByLastIndex > 0 && line.startsWith("at ") && index < causedByLastIndex) { + return@filterIndexed false + } + !line.startsWith("at java.desktop/") + && !line.startsWith("at java.base/") + && !line.startsWith("at kotlin.") + && !line.startsWith("at kotlinx.") + && !line.startsWith("at com.intellij.") }.joinToString("\n"), /*"device-os" to with(System.getProperty("os.name").lowercase()) { when { // Windows, macOS or Linux @@ -63,9 +84,9 @@ class GitHubErrorReportSubmitter : ErrorReportSubmitter() { else -> "Linux" } },*/ // currently cannot be set (https://github.com/orgs/community/discussions/44983) - "additional-device-info" to getDefaultHelpBlock(project) - ).filterValues { it.isNotEmpty() }) - ) + "additional-device-info" to getPlatformAndPluginsInfo(project) + ).filterValues { it.isNotEmpty() } + )) consumer.consume(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.NEW_ISSUE)) true } catch (e: Exception) { @@ -74,54 +95,82 @@ class GitHubErrorReportSubmitter : ErrorReportSubmitter() { } } + /** + * Build the URL for the GitHub issue from the given fields, abbreviating the URL if necessary + * to fit within the maximum URL length. + * + * @param fields the fields to include in the URL. + * @return the URL for the GitHub issue. + */ private fun buildAbbreviatedUrl(fields: Map): URI { val url = buildUrl(fields) - return URI(if (url.length > MAX_URL_LENGTH) { - val newMap = fields.toMutableMap() - newMap[BUG_LOGS_KEY]?.let { fullLog -> - val logLessUrlLength = buildUrl(fields.mapValues { (key, value) -> - if (key == BUG_LOGS_KEY) "" else value - }).length - val encodedLogDiff = URLEncoder.encode(fullLog, StandardCharsets.UTF_8).length - fullLog.length - newMap[BUG_LOGS_KEY] = fullLog.take( - (MAX_URL_LENGTH - logLessUrlLength - encodedLogDiff).coerceAtLeast(fullLog.substringBefore("\n").length) - ).run { - if (length > fullLog.substringBefore("\n").length + TRIMMED_STACKTRACE_MARKER.length) { - "${take(length - TRIMMED_STACKTRACE_MARKER.length)}$TRIMMED_STACKTRACE_MARKER" - } else this + return URI( + if (url.length > MAX_URL_LENGTH) { + val newMap = fields.toMutableMap() + newMap[BUG_LOGS_KEY]?.let { fullLog -> + val logLessUrlLength = buildUrl(fields.mapValues { (key, value) -> + if (key == BUG_LOGS_KEY) "" else value + }).length + val encodedLogDiff = URLEncoder.encode(fullLog, StandardCharsets.UTF_8).length - fullLog.length + newMap[BUG_LOGS_KEY] = fullLog.take( + (MAX_URL_LENGTH - logLessUrlLength - encodedLogDiff).coerceAtLeast(fullLog.substringBefore("\n").length) + ).run { + if (length > fullLog.substringBefore("\n").length + TRIMMED_STACKTRACE_MARKER.length) { + "${take(length - TRIMMED_STACKTRACE_MARKER.length)}$TRIMMED_STACKTRACE_MARKER" + } else this + } } - } - val shorterLogUrl = buildUrl(newMap) - if (shorterLogUrl.length > MAX_URL_LENGTH) { - buildUrl(fields.filter { (key, _) -> - key == "title" || key == "additional-device-info" - }) - } else shorterLogUrl - } else url + val shorterLogUrl = buildUrl(newMap) + if (shorterLogUrl.length > MAX_URL_LENGTH) { + buildUrl(fields.filter { (key, _) -> + key == "title" || key == "additional-device-info" + }) + } else shorterLogUrl + } else url ) } + /** + * Build the URL for the GitHub issue from the given fields. + * + * @param fields the fields to include in the URL. + * @return the URL for the GitHub issue. + */ private fun buildUrl(fields: Map) = buildString { - append("https://github.com/WarningImHack3r/intellij-shadcn-plugin/issues/new?labels=bug&template=bug_report.yml") + append("$REPO_URL/issues/new?labels=bug&template=bug_report.yml") fields.forEach { (key, value) -> append("&$key=${URLEncoder.encode(value, StandardCharsets.UTF_8)}") } } - private fun getDefaultHelpBlock(project: Project): String { + /** + * Get the platform and plugins information for the given project. + * Used in the "Additional platform info" section of the GitHub issue. + * + * @param project the [Project][com.intellij.openapi.project.Project] to get the platform and plugins information from. + * @return the platform and plugins information for the given project. + */ + private fun getPlatformAndPluginsInfo(project: Project): String { return CompositeGeneralTroubleInfoCollector().collectInfo(project).run { val trimmedAndCleaned = split("\n".toRegex()).filter { trim().isNotEmpty() } // Build, JRE, JVM, OS - trimmedAndCleaned - .dropWhile { s -> s == "=== About ===" } - .takeWhile { s -> s != "=== System ===" } - .filter { s -> !s.startsWith("idea.") && !s.startsWith("Theme") } - .joinToString("\n") + "\n" + - // Plugins + buildString { + append( + trimmedAndCleaned + .dropWhile { s -> s == "=== About ===" } + .takeWhile { s -> s != "=== System ===" } + .filter { s -> !s.startsWith("idea.") && !s.startsWith("Theme") } + .joinToString("\n") + ) + append("\n") + // Plugins + append( trimmedAndCleaned .dropWhile { s -> s != "=== Plugins ===" } .takeWhile { s -> s.isNotBlank() && s.isNotEmpty() } .joinToString("\n") + ) + } } } @@ -132,12 +181,10 @@ class GitHubErrorReportSubmitter : ErrorReportSubmitter() { * @return the [Project][com.intellij.openapi.project.Project] that was last in focus or open. */ private fun getLastFocusedOrOpenedProject(): Project { - val project = IdeFocusManager.getGlobalInstance().lastFocusedFrame?.project - if (project == null) { + return IdeFocusManager.getGlobalInstance().lastFocusedFrame?.project ?: run { val projectManager = ProjectManager.getInstance() val openProjects = projectManager.openProjects - return if (openProjects.isNotEmpty()) openProjects.first() else projectManager.defaultProject + if (openProjects.isNotEmpty()) openProjects.first() else projectManager.defaultProject } - return project } }