OPENIDE #49 Implement sending default errors from OpenIDE

This commit is contained in:
Nikita Iarychenko
2024-12-10 09:21:11 +00:00
parent 98a3ac5ed9
commit d7b7726939
7 changed files with 225 additions and 21 deletions

18
.idea/libraries/io_sentry.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<component name="libraryTable">
<library name="io.sentry" type="repository">
<properties maven-id="io.sentry:sentry:7.8.0">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/io/sentry/sentry/7.8.0/sentry-7.8.0.jar">
<sha256sum>927f0dfd76c9262f3d755345d72468138ae1bdba5d8f20c44f79ba2e1efcdb91</sha256sum>
</artifact>
</verification>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/sentry/sentry/7.8.0/sentry-7.8.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/sentry/sentry/7.8.0/sentry-7.8.0-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@@ -141,5 +141,6 @@
<orderEntry type="module" module-name="intellij.platform.resources" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.resources.en" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.ide.ui" />
<orderEntry type="library" name="io.sentry" level="project" />
</component>
</module>

View File

@@ -22,28 +22,28 @@ error.list.message.submitted=Submitted
error.list.message.submitted.as.link=Submitted as <a href="{0}">{1}</a>
error.list.message.submission.failed=Submission failed
error.list.message.submission.failed.details=Submission failed ({0})
error.dialog.foreign.plugin.warning=This plugin is not a production of JetBrains. Please report the problem to <a href="{1}">{0}</a>.
error.dialog.foreign.plugin.warning.unnamed=This plugin is not a production of JetBrains. Please report the problem to the <a href="{0}">plugin vendor</a>.
error.dialog.foreign.plugin.warning.unknown=This plugin is not a production of JetBrains. Please report the problem to the plugin vendor.
error.dialog.foreign.plugin.warning=This plugin is not a production of OpenIDE. Please report the problem to <a href="{1}">{0}</a>.
error.dialog.foreign.plugin.warning.unnamed=This plugin is not a production of OpenIDE. Please report the problem to the <a href="{0}">plugin vendor</a>.
error.dialog.foreign.plugin.warning.unknown=This plugin is not a production of OpenIDE. Please report the problem to the plugin vendor.
error.dialog.comment.prompt=Comment on what you were doing when the exception occurred
error.dialog.notice.label=By submitting this report, you agree to the terms and conditions of the privacy policy
error.dialog.notice.label.expanded=By submitting this report, you agree to the terms and conditions of the privacy policy
error.dialog.submit.anonymous=Submit anonymously or <a href="#">Log In</a>
error.dialog.submit.named=Submit as <b>{0}</b> or <a href="#">change account</a>
error.report.to.jetbrains.action=&Report to JetBrains
error.report.to.jetbrains.action=&Report to OpenIDE
error.report.all.action=Report All
error.report.and.clear.all.action=Report and Clear All
error.report.impossible.action=Report Exception
error.report.impossible.tooltip=The plugin vendor didn't implement the exception reporting component.
error.dialog.clear.all.action=&Clear all
error.dialog.notice.anonymous=I agree to my hardware configuration, software configuration, product information, and the error details shown above \
being used by JetBrains s.r.o. ("JetBrains") to let JetBrains improve its products and to provide me with support service \
in accordance with the end-user agreement of <a href="https://www.jetbrains.com/legal/agreements/exception_analyzer.html">JetBrains Exception Analyzer</a>.
being used by OpenIDE to let OpenIDE improve its products and to provide me with support service \
in accordance with the end-user agreement of <a href="https://openide.ru/legal/agreements/exception_analyzer.html">OpenIDE Exception Analyzer</a>.
error.dialog.notice.third-party.plugin.exception=I agree that my hardware configuration, software configuration, product information, and the error details shown above\
\ might be used by Jetbrains s.r.o. (\u201CJetBrains\u201D) and the author of the plugin to improve their products and provide me with support service by JetBrains\
\ in accordance with the relevant <a href="https://www.jetbrains.com/legal/agreements/exception_analyzer.html">Agreement</a>.\
\ If you submit the report containing any personal data, please note that such data will be shared with JetBrains and the plugin author.\
\ JetBrains is not responsible for any behaviour of any third-party plugins and their vendors, including processing of your personal data.
\ might be used by OpenIDE and the author of the plugin to improve their products and provide me with support service by OpenIDE\
\ in accordance with the relevant <a href="https://openide.ru/legal/agreements/exception_analyzer.html">Agreement</a>.\
\ If you submit the report containing any personal data, please note that such data will be shared with OpenIDE and the plugin author.\
\ OpenIDE is not responsible for any behaviour of any third-party plugins and their vendors, including processing of your personal data.
error.dialog.notice.third-party.plugin.send=Report to the Third-Party Plugin
title.submitting.error.report=Submitting error report
error.report.submitted=Report submitted

View File

@@ -94,16 +94,15 @@ open class ITNReporter(private val postUrl: String = "") : ErrorReportSubmitter(
private fun submit(project: Project?, errorBean: ErrorBean, parentComponent: Component, callback: (SubmittedReportInfo) -> Unit): Boolean {
service<ITNProxyCoroutineScopeHolder>().coroutineScope.launch {
try {
val reportId = if (project != null) {
if (project != null) {
withBackgroundProgress(project, DiagnosticBundle.message("title.submitting.error.report")) {
ITNProxy.sendError(errorBean, postUrl)
ITNUtils.sendError(errorBean)
}
}
else {
ITNProxy.sendError(errorBean, postUrl)
ITNUtils.sendError(errorBean)
}
updatePreviousReport(errorBean.event, reportId)
onSuccess(project, reportId, callback)
onSuccess(project, callback)
}
catch (e: Exception) {
onError(project, e, errorBean, parentComponent, callback)
@@ -112,15 +111,11 @@ open class ITNReporter(private val postUrl: String = "") : ErrorReportSubmitter(
return true
}
private fun onSuccess(project: Project?, reportId: Int, callback: (SubmittedReportInfo) -> Unit) {
val reportUrl = ITNProxy.getBrowseUrl(reportId)
callback(SubmittedReportInfo(reportUrl, reportId.toString(), SubmittedReportInfo.SubmissionStatus.NEW_ISSUE))
private fun onSuccess(project: Project?, callback: (SubmittedReportInfo) -> Unit) {
callback(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.NEW_ISSUE))
val content = DiagnosticBundle.message("error.report.gratitude")
val title = DiagnosticBundle.message("error.report.submitted")
val notification = Notification("Error Report", title, content, NotificationType.INFORMATION).setImportant(false)
if (reportUrl != null) {
notification.addAction(NotificationAction.createSimpleExpiring(DiagnosticBundle.message("error.report.view.action")) { BrowserUtil.browse(reportUrl) })
}
notification.notify(project)
}

View File

@@ -0,0 +1,118 @@
// Copyright (c) Haulmont 2024. All Rights Reserved.
// Use is subject to license terms.
package com.intellij.diagnostic
import com.intellij.ide.ApplicationActivity
import com.intellij.idea.IdeaLogger
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.ObjectUtils
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.protocol.SentryRuntime
import io.sentry.protocol.User
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
object ITNUtils {
val log = Logger.getInstance(ITNUtils::class.java)
internal fun sendError(error: ITNProxy.ErrorBean) {
val event = SentryEvent()
event.level = SentryLevel.ERROR
event.throwable = (error.event as IdeaReportingEvent).data.throwable
event.release = ApplicationInfo.getInstance().build.asString()
event.user = User().apply {
id = uniqueComputerId
}
// set server name to empty to avoid tracking personal data
event.serverName = SystemInfo.getOsNameAndVersion()
event.setExtra("ide.is_eap", ApplicationManager.getApplication().isEAP)
event.setExtra("ide.is_internal", ApplicationManager.getApplication().isInternal)
val sentryRuntime = SentryRuntime()
val javaVendor = System.getProperty("java.vendor", "<unknown vendor>")
val javaVersion = System.getProperty("java.version", "<unknown version>")
sentryRuntime.name = "$javaVendor $javaVersion"
event.contexts.setRuntime(sentryRuntime)
val lastActionId = IdeaLogger.ourLastActionId
if (StringUtil.isNotEmpty(lastActionId)) {
event.setExtra("Last action", lastActionId)
}
if (StringUtil.isNotEmpty(error.pluginId)) {
event.setExtra("plugin.id", error.pluginId!!)
}
if (StringUtil.isNotEmpty(error.pluginVersion)) {
event.setExtra("plugin.version", error.pluginVersion!!)
}
if (StringUtil.isNotEmpty(error.pluginName)) {
event.setExtra("plugin.name", error.pluginName!!)
}
event.setExtra("Memory", (Runtime.getRuntime().maxMemory() / FileUtilRt.MEGABYTE).toString() + "M")
if (SystemInfo.isUnix && !SystemInfo.isMac) {
event.setExtra("Current Desktop", ObjectUtils.notNull(System.getenv("XDG_CURRENT_DESKTOP"), "Undefined"))
}
Sentry.captureEvent(event)
}
val uniqueComputerId: String get() = UniqueComputerId.value
fun getEnvironment() = when {
isSnapshot() -> "snapshot"
else -> "production"
}
private fun isSnapshot() = ApplicationInfo.getInstance().build.asString()
.contains("snapshot", ignoreCase = true)
}
/**
* Makes Sentry initialization.
*/
class SentryInitializer : ApplicationActivity {
private val isHeadless by lazy {
ApplicationManager.getApplication().run { isHeadlessEnvironment || isUnitTestMode }
}
private val isInitialized = AtomicBoolean(false)
override suspend fun execute() {
if (isHeadless) return
val isInitializedBefore = isInitialized.getAndSet(true)
if (isInitializedBefore) return
initComputerId()
Sentry.init { options: SentryOptions ->
options.dsn = "https://73728607f7e2a336e7cade498caaae0c@sentry-amp.haulmont.com/4"
options.isEnableUncaughtExceptionHandler = false
options.setModulesLoader { emptyMap() }
options.environment = ITNUtils.getEnvironment()
}
}
private suspend fun initComputerId() = coroutineScope {
launch(Dispatchers.IO) {
UniqueComputerId.precompute()
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Haulmont 2024. All Rights Reserved.
// Use is subject to license terms.
package com.intellij.diagnostic
import com.intellij.internal.statistic.eventLog.fus.MachineIdManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.SystemProperties
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.io.DigestUtil
import com.intellij.util.io.bytesToHex
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.*
object UniqueComputerId {
private val log = logger<UniqueComputerId>()
@RequiresBackgroundThread
fun precompute() {
// call our lazy value
value
}
val value by lazy {
val configFile = resolveUserIdFile()
loadIdFromFile(configFile) ?: generateIdAndWrite(configFile)
}
private fun loadIdFromFile(configFile: File): String? {
if (!configFile.exists()) {
return null
}
return try {
val fileSource = FileUtil.loadFile(configFile, StandardCharsets.UTF_8)
fileSource.trim().ifEmpty { null }
} catch (e: IOException) {
log.warn("Exception during read user id", e)
null
}
}
private fun generateIdAndWrite(configFile: File): String {
val id = getAnonymizedMachineId()
try {
FileUtil.writeToFile(configFile, id, StandardCharsets.UTF_8)
} catch (e: IOException) {
log.warn("Exception during write user id", e)
}
return id
}
private fun getAnonymizedMachineId(): String {
var machineId = MachineIdManager.getAnonymizedMachineId("openide.unique.computer.id")
if (machineId != null) {
return machineId
}
log.warn("Getting machine id was failed")
machineId = UUID.randomUUID().toString()
val result = "${System.getProperty("user.name")}-$machineId"
val md = DigestUtil.sha256()
md.update(result.toByteArray())
return bytesToHex(md.digest())
}
private fun resolveUserIdFile(): File {
return File(SystemProperties.getUserHome(), ".openide/user-id.txt")
}
}

View File

@@ -1870,6 +1870,7 @@
<applicationService serviceInterface="com.intellij.auth.GenericAuthService"
serviceImplementation="com.intellij.auth.LocalGenericAuthService"
client="local"/>
<applicationActivity implementation="com.intellij.diagnostic.SentryInitializer"/>
<applicationService serviceInterface="com.intellij.platform.ide.provisioner.ProvisionedServiceRegistry"
serviceImplementation="com.intellij.platform.ide.provisioner.DefaultProvisionedServiceRegistry"/>
<applicationService serviceInterface="com.intellij.platform.ide.provisioner.ProvisionerCompanyBrandingProvider"