Prevent constant reporting from released plugin versions in EA (KT-30388)

Apply a compromise strategy between "no report" and "always report".
Report only for several days after release and don't bother users afterwards.

 #KT-30388 Fixed

All exceptions still can be found in Idea Log.
This commit is contained in:
Nikolay Krasko
2019-02-28 19:13:58 +03:00
parent 7f07782095
commit 7886095327
3 changed files with 230 additions and 4 deletions
@@ -27,13 +27,18 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Alarm
import com.intellij.util.io.HttpRequests
import com.intellij.util.text.VersionComparatorUtil
import org.jetbrains.kotlin.idea.update.PluginUpdateVerifier
import org.jdom.Attribute
import org.jdom.DataConversionException
import org.jetbrains.kotlin.idea.update.verify
import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URLEncoder
import java.time.DateTimeException
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
sealed class PluginUpdateStatus {
@@ -322,5 +327,46 @@ class KotlinPluginUpdater(val propertiesComponent: PropertiesComponent) : Dispos
private val LOG = Logger.getInstance(KotlinPluginUpdater::class.java)
fun getInstance(): KotlinPluginUpdater = ServiceManager.getService(KotlinPluginUpdater::class.java)
class ResponseParseException(message: String, cause: Exception? = null) : IllegalStateException(message, cause)
@Throws(IOException::class, ResponseParseException::class)
fun fetchPluginReleaseDate(pluginId: String, version: String): LocalDate? {
// Need a better request (MP-2157)
val url = "https://plugins.jetbrains.com/plugins/list?pluginId=$pluginId&pluginVersion=$version"
val responseDoc = HttpRequests.request(url).connect {
JDOMUtil.load(it.inputStream)
}
if (responseDoc.name != "plugin-repository") {
throw ResponseParseException("No plugin repository element")
}
val dateAttribute: Attribute = responseDoc
.getChild("category")
?.getChildren("idea-plugin")
?.mapNotNull { pluginElement ->
if (pluginElement.getChild("version")?.text == version) {
pluginElement.getAttribute("date")
} else {
null
}
}
?.singleOrNull()
?: throw ResponseParseException("Can't find current plugin version")
val dateLong = try {
dateAttribute.longValue
} catch (e: DataConversionException) {
throw ResponseParseException("Can't convert date value to long", e)
}
return try {
Instant.ofEpochMilli(dateLong).atZone(ZoneOffset.UTC).toLocalDate()
} catch (e: DateTimeException) {
throw ResponseParseException("Can't convert to date", e)
}
}
}
}
@@ -29,6 +29,7 @@ import com.intellij.openapi.updateSettings.impl.UpdateChecker;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.search.searches.IndexPatternSearch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.idea.reporter.KotlinReportSubmitter;
import org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinTodoSearcher;
import org.jetbrains.kotlin.utils.PathUtil;
@@ -82,6 +83,8 @@ public class PluginStartupComponent implements ApplicationComponent {
KotlinPluginCompatibilityVerifier.checkCompatibility();
KotlinReportSubmitter.Companion.setupReportingFromRelease();
//todo[Sedunov]: wait for fix in platform to avoid misunderstood from Java newbies (also ConfigureKotlinInTempDirTest)
//KotlinSdkType.Companion.setUpIfNeeded();
}
@@ -18,30 +18,204 @@ package org.jetbrains.kotlin.idea.reporter
import com.intellij.diagnostic.ReportMessages
import com.intellij.ide.DataManager
import com.intellij.ide.util.PropertiesComponent
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.IdeaLoggingEvent
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.SubmittedReportInfo
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.util.Consumer
import com.intellij.util.ThreeState
import org.jetbrains.kotlin.idea.KotlinPluginUpdater
import org.jetbrains.kotlin.idea.KotlinPluginUtil
import org.jetbrains.kotlin.idea.PluginUpdateStatus
import org.jetbrains.kotlin.idea.util.isEap
import java.awt.Component
import java.io.IOException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.time.temporal.ChronoUnit
import javax.swing.Icon
/**
* We need to wrap ITNReporter for force showing or errors from kotlin plugin even from released version of IDEA.
*/
class KotlinReportSubmitter : ITNReporterCompat() {
companion object {
private const val KOTLIN_FATAL_ERROR_NOTIFICATION_PROPERTY = "kotlin.fatal.error.notification"
private const val IDEA_FATAL_ERROR_NOTIFICATION_PROPERTY = "idea.fatal.error.notification"
private const val DISABLED_VALUE = "disabled"
private const val ENABLED_VALUE = "enabled"
private const val KOTLIN_PLUGIN_RELEASE_DATE = "kotlin.plugin.releaseDate"
private val LOG = Logger.getInstance(KotlinReportSubmitter::class.java)
@Volatile
private var isFatalErrorReportingDisabledInRelease = ThreeState.UNSURE
private val isIdeaAndKotlinRelease by lazy {
// Disabled in released version of IDEA and Android Studio
// Enabled in EAPs, Canary and Beta
val isReleaseLikeIdea = DISABLED_VALUE == System.getProperty(IDEA_FATAL_ERROR_NOTIFICATION_PROPERTY, ENABLED_VALUE)
val isKotlinRelease =
!(KotlinPluginUtil.isSnapshotVersion() || KotlinPluginUtil.isDevVersion() || isEap(KotlinPluginUtil.getPluginVersion()))
isReleaseLikeIdea && isKotlinRelease
}
private const val NUMBER_OF_REPORTING_DAYS_FROM_RELEASE = 7
fun setupReportingFromRelease() {
if (ApplicationManager.getApplication().isUnitTestMode) {
return
}
if (!isIdeaAndKotlinRelease) {
return
}
val currentPluginReleaseDate = readStoredPluginReleaseDate()
if (currentPluginReleaseDate != null) {
isFatalErrorReportingDisabledInRelease =
if (isFatalErrorReportingDisabled(currentPluginReleaseDate)) ThreeState.YES else ThreeState.NO
return
}
ApplicationManager.getApplication().executeOnPooledThread {
val releaseDate =
try {
KotlinPluginUpdater.fetchPluginReleaseDate(
KotlinPluginUtil.KOTLIN_PLUGIN_ID.idString,
KotlinPluginUtil.getPluginVersion()
)
} catch (e: IOException) {
// Do not report connection problems
null
} catch (e: KotlinPluginUpdater.Companion.ResponseParseException) {
// Exception won't be shown, but will be logged
LOG.error(e)
return@executeOnPooledThread
}
if (releaseDate != null) {
writePluginReleaseValue(releaseDate)
} else {
// Will try to fetch the same release date on IDE restart
}
isFatalErrorReportingDisabledInRelease = isFatalErrorReportingWithDefault(releaseDate)
}
}
private fun isFatalErrorReportingWithDefault(releaseDate: LocalDate?): ThreeState {
return if (releaseDate != null) {
if (isFatalErrorReportingDisabled(releaseDate)) ThreeState.YES else ThreeState.NO
} else {
// Disable reporting by default until we obtain a valid release date.
// We might fail reporting exceptions that happened before initialization but after successful release date fetching
// such exceptions maybe be reported after restart.
ThreeState.YES
}
}
private fun isFatalErrorReportingDisabledWithUpdate(): Boolean {
val currentPluginReleaseDate = readStoredPluginReleaseDate()
isFatalErrorReportingDisabledInRelease = isFatalErrorReportingWithDefault(currentPluginReleaseDate)
return isFatalErrorReportingDisabledInRelease == ThreeState.YES
}
private fun isFatalErrorReportingDisabled(releaseDate: LocalDate): Boolean {
return ChronoUnit.DAYS.between(releaseDate, LocalDate.now()) > NUMBER_OF_REPORTING_DAYS_FROM_RELEASE
}
private val RELEASE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd")
private fun readStoredPluginReleaseDate(): LocalDate? {
val pluginVersionToReleaseDate = PropertiesComponent.getInstance().getValue(KOTLIN_PLUGIN_RELEASE_DATE) ?: return null
val parsedDate = fun(): LocalDate? {
val parts = pluginVersionToReleaseDate.split(":")
if (parts.size != 2) {
return null
}
val pluginVersion = parts[0]
if (pluginVersion != KotlinPluginUtil.getPluginVersion()) {
// Stored for some other plugin version
return null
}
return try {
val dateString = parts[1]
LocalDate.parse(dateString, RELEASE_DATE_FORMATTER)
} catch (e: DateTimeParseException) {
null
}
}.invoke()
if (parsedDate == null) {
PropertiesComponent.getInstance().setValue(KOTLIN_PLUGIN_RELEASE_DATE, null)
}
return parsedDate
}
private fun writePluginReleaseValue(date: LocalDate) {
val currentKotlinVersion = KotlinPluginUtil.getPluginVersion()
val dateStr = RELEASE_DATE_FORMATTER.format(date)
PropertiesComponent.getInstance().setValue(KOTLIN_PLUGIN_RELEASE_DATE, "$currentKotlinVersion:$dateStr")
}
}
private var hasUpdate = false
private var hasLatestVersion = false
override fun showErrorInRelease(event: IdeaLoggingEvent): Boolean {
val notificationEnabled = "disabled" != System.getProperty("kotlin.fatal.error.notification", "enabled")
return notificationEnabled && (!hasUpdate || ApplicationManager.getApplication().isInternal)
if (ApplicationManager.getApplication().isInternal) {
// Reporting is always enabled for internal mode in the platform
return true
}
if (ApplicationManager.getApplication().isUnitTestMode) {
return true
}
if (hasUpdate) {
return false
}
val kotlinNotificationEnabled = DISABLED_VALUE != System.getProperty(KOTLIN_FATAL_ERROR_NOTIFICATION_PROPERTY, ENABLED_VALUE)
if (!kotlinNotificationEnabled) {
// Kotlin notifications are explicitly disabled
return false
}
if (!isIdeaAndKotlinRelease) {
return true
}
return when (isFatalErrorReportingDisabledInRelease) {
ThreeState.YES ->
false
ThreeState.NO -> {
// Reiterate the check for the case when there was no restart for long and reporting decision might expire
!isFatalErrorReportingDisabledWithUpdate()
}
ThreeState.UNSURE -> {
// There might be an exception while initialization isn't complete.
// Decide urgently based on previously stored release version if already fetched.
!isFatalErrorReportingDisabledWithUpdate()
}
}
}
override fun submitCompat(
@@ -54,6 +228,8 @@ class KotlinReportSubmitter : ITNReporterCompat() {
if (ApplicationManager.getApplication().isInternal) {
return super.submitCompat(events, additionalInfo, parentComponent, consumer)
}
// TODO: What happens here? User clicks report but no report is send?
return true
}
@@ -68,7 +244,8 @@ class KotlinReportSubmitter : ITNReporterCompat() {
ReportMessages.ERROR_REPORT,
"Can't report exception from patched plugin",
NotificationType.INFORMATION,
null)
null
)
.setImportant(false)
.notify(project)
return true