diff --git a/build.sbt b/build.sbt index cf1fe570358..293f49df7a4 100644 --- a/build.sbt +++ b/build.sbt @@ -138,6 +138,7 @@ commands ++= Seq( runMtagsPublishLocal(st, v, localSnapshotVersion) } "interfaces/publishLocal" :: + "+telemetryInterfaces/publishLocal" :: s"++${V.scala213} metals/publishLocal" :: "mtags-java/publishLocal" :: publishMtags @@ -181,6 +182,7 @@ def lintingOptions(scalaVersion: String) = { "-Wconf:src=*.TreeViewProvider.scala&msg=parameter params in method (children|parent) is never used:silent", // silence "The outer reference in this type test cannot be checked at run time." "-Wconf:src=.*(CompletionProvider|ArgCompletions|Completions|Keywords|IndentOnPaste).scala&msg=The outer reference:silent", + "-Wconf:src=.*(SourceCodeSanitizer).scala&msg=Unused import:silent", ) crossSetting( scalaVersion, @@ -208,7 +210,7 @@ val sharedScalacOptions = List( isScala212(partialVersion) && V.scala212 != scalaVersion.value => List("-target:jvm-1.8", "-Yrangepos", "-Xexperimental") case partialVersion if isScala3(partialVersion) => - List("-Xtarget:8", "-language:implicitConversions", "-Xsemanticdb") + List("-Xtarget:8", "-language:implicitConversions") case _ => List("-target:jvm-1.8", "-Yrangepos") } @@ -251,6 +253,26 @@ lazy val interfaces = project ), ) +lazy val telemetryInterfaces = project + .in(file("telemetry-interfaces")) + .settings(sharedSettings) + .settings( + moduleName := "telemetry-interfaces", + crossScalaVersions := List(V.scala213, V.scala3), + crossVersion := CrossVersion.binary, + Compile / scalacOptions ++= { + if (scalaVersion.value == V.scala3) + List("-Xmax-inlines", "64") + else Nil + }, + libraryDependencies := List( + "com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.0", + "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.10.0", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.27.7", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.27.7" % "compile-internal", + ), + ) + lazy val mtagsShared = project .in(file("mtags-shared")) .settings(sharedSettings) @@ -265,7 +287,12 @@ lazy val mtagsShared = project Compile / packageSrc / publishArtifact := true, Compile / scalacOptions ++= { if (scalaVersion.value == V.scala3) - List("-Yexplicit-nulls", "-language:unsafeNulls") + List( + "-Yexplicit-nulls", + "-language:unsafeNulls", + "-Xfatal-warnings", + "-deprecation", + ) else Nil }, libraryDependencies ++= List( @@ -502,7 +529,9 @@ lazy val metals = project // for JSON formatted doctor "com.lihaoyi" %% "ujson" % "3.1.5", // For fetching projects' templates - "com.lihaoyi" %% "requests" % "0.8.0", + // telemetry client + "com.softwaremill.sttp.client3" %% "core" % "3.9.5", + "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "1.10.0", // for producing SemanticDB from Scala source files, to be sure we want the same version of scalameta "org.scalameta" %% "scalameta" % V.semanticdb(scalaVersion.value), "org.scalameta" % "semanticdb-scalac-core" % V.semanticdb( @@ -557,7 +586,7 @@ lazy val metals = project "lastSupportedSemanticdb" -> SemanticDbSupport.last, ), ) - .dependsOn(mtags, `mtags-java`) + .dependsOn(mtags, `mtags-java`, telemetryInterfaces) .enablePlugins(BuildInfoPlugin) lazy val `sbt-metals` = project @@ -805,6 +834,7 @@ lazy val unit = project "io.get-coursier" %% "coursier" % V.coursier, // for jars "ch.epfl.scala" %% "bloop-config" % V.bloopConfig, "org.scalameta" %% "munit" % V.munit, + "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0", ), buildInfoPackage := "tests", Compile / resourceGenerators += InputProperties diff --git a/metals-bench/src/main/scala/bench/MetalsBench.scala b/metals-bench/src/main/scala/bench/MetalsBench.scala index 5715745a5de..98379b00390 100644 --- a/metals-bench/src/main/scala/bench/MetalsBench.scala +++ b/metals-bench/src/main/scala/bench/MetalsBench.scala @@ -8,7 +8,6 @@ import scala.meta.dialects import scala.meta.interactive.InteractiveSemanticdb import scala.meta.internal.metals.EmptyReportContext import scala.meta.internal.metals.JdkSources -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.logging.MetalsLogger import scala.meta.internal.mtags.JavaMtags import scala.meta.internal.mtags.Mtags @@ -22,6 +21,7 @@ import scala.meta.internal.tokenizers.LegacyScanner import scala.meta.internal.tokenizers.LegacyToken import scala.meta.io.AbsolutePath import scala.meta.io.Classpath +import scala.meta.pc.ReportContext import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode diff --git a/metals/src/main/scala/scala/meta/internal/bsp/ConnectionBspStatus.scala b/metals/src/main/scala/scala/meta/internal/bsp/ConnectionBspStatus.scala index 7f8f2ecb315..cbdca88c74c 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/ConnectionBspStatus.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/ConnectionBspStatus.scala @@ -7,10 +7,10 @@ import scala.meta.internal.metals.BspStatus import scala.meta.internal.metals.ClientCommands import scala.meta.internal.metals.Icons import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.clients.language.MetalsStatusParams import scala.meta.internal.metals.clients.language.StatusType import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext class ConnectionBspStatus( bspStatus: BspStatus, @@ -102,7 +102,7 @@ class ConnectionBspStatus( */ private def syncWithReportContext(errorReports: Set[String]) = errorReports.intersect( - rc.bloop.getReports().map(_.toPath.toUri().toString()).toSet + rc.bloop.getReports().map(_.toPath.toUri().toString()).asScala.toSet ) } diff --git a/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala b/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala index d968ecc8663..888928b219d 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala @@ -5,9 +5,9 @@ import java.security.MessageDigest import scala.meta.internal.bsp.BspSession import scala.meta.internal.bsp.ConnectionBspStatus import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.Tables +import scala.meta.internal.pc.StandardReport +import scala.meta.pc.ReportContext import com.google.common.io.BaseEncoding @@ -19,7 +19,7 @@ class BspErrorHandler( def onError(message: String): Unit = { if (shouldShowBspError) { for { - report <- createReport(message) + report <- createReport(message).asScala if !tables.dismissedNotifications.BspErrors.isDismissed } bspStatus.showError(message, report) } else logError(message) @@ -36,7 +36,7 @@ class BspErrorHandler( val id = BaseEncoding.base64().encode(digest) val sanitized = reportContext.bloop.sanitize(message) reportContext.bloop.create( - Report( + StandardReport( sanitized.trimTo(20), s"""|### Bloop error: | diff --git a/metals/src/main/scala/scala/meta/internal/builds/NewProjectProvider.scala b/metals/src/main/scala/scala/meta/internal/builds/NewProjectProvider.scala index c4092f5dc4a..2a35ab97d5d 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/NewProjectProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/NewProjectProvider.scala @@ -23,6 +23,7 @@ import scala.meta.internal.process.ExitCodes import scala.meta.io.AbsolutePath import coursierapi._ +import sttp.client3._ class NewProjectProvider( client: MetalsLanguageClient, @@ -34,13 +35,15 @@ class NewProjectProvider( )(implicit context: ExecutionContext) { private val templatesUrl = - "https://github.com/foundweekends/giter8/wiki/giter8-templates.md" + uri"https://github.com/foundweekends/giter8/wiki/giter8-templates.md" private val giterDependency = Dependency .of("org.foundweekends.giter8", "giter8_2.12", BuildInfo.gitter8Version) // equal to cmd's: g8 playframework/play-scala-seed.g8 --name=../<> private val giterMain = "giter8.Giter8" + val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() private var allTemplates = Seq.empty[MetalsQuickPickItem] + def allTemplatesFromWeb: Seq[MetalsQuickPickItem] = synchronized { if (allTemplates.nonEmpty) { @@ -53,14 +56,17 @@ class NewProjectProvider( // - [jimschubert/finatra.g8](https://github.com/jimschubert/finatra.g8) // (A simple Finatra 2.5 template with sbt-revolver and sbt-native-packager) val all = for { - result <- Try(requests.get(templatesUrl)).toOption.toIterable - _ = if (result.statusCode != 200) + result <- Try( + basicRequest.get(templatesUrl).send(backend) + ).toOption.toIterable + _ = if (!result.is200) client.showMessage( - NewScalaProject.templateDownloadFailed(result.statusMessage) + NewScalaProject.templateDownloadFailed(result.statusText) ) - if result.statusCode == 200 + if result.is200 + text <- result.body.toOption } yield { - NewProjectProvider.templatesFromText(result.text(), icons.github) + NewProjectProvider.templatesFromText(text, icons.github) } allTemplates = all.flatten.toSeq } diff --git a/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala b/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala index 39c5c9371c1..245b4e96423 100644 --- a/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala @@ -15,8 +15,6 @@ import scala.meta.internal.metals.BuildTargets import scala.meta.internal.metals.Compilers import scala.meta.internal.metals.DefinitionProvider import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.ScalaVersionSelector import scala.meta.internal.metals.ScalaVersions import scala.meta.internal.metals.SemanticdbFeatureProvider @@ -29,6 +27,7 @@ import scala.meta.internal.mtags.Semanticdbs import scala.meta.internal.mtags.UnresolvedOverriddenSymbol import scala.meta.internal.parsing.Trees import scala.meta.internal.pc.PcSymbolInformation +import scala.meta.internal.pc.StandardReport import scala.meta.internal.search.SymbolHierarchyOps._ import scala.meta.internal.semanticdb.ClassSignature import scala.meta.internal.semanticdb.Scala.Descriptor.Method @@ -42,6 +41,7 @@ import scala.meta.internal.semanticdb.TypeSignature import scala.meta.io.AbsolutePath import scala.meta.pc.PcSymbolKind import scala.meta.pc.PcSymbolProperty +import scala.meta.pc.ReportContext import ch.epfl.scala.bsp4j.BuildTargetIdentifier import org.eclipse.lsp4j.Location @@ -176,7 +176,7 @@ final class ImplementationProvider( if (sourceFiles.isEmpty) { rc.unsanitized.create( - Report( + StandardReport( "missing-definition", s"""|Missing definition symbol for: |$dealisedSymbol diff --git a/metals/src/main/scala/scala/meta/internal/implementation/Supermethods.scala b/metals/src/main/scala/scala/meta/internal/implementation/Supermethods.scala index 2bef1344c08..64b6dd71596 100644 --- a/metals/src/main/scala/scala/meta/internal/implementation/Supermethods.scala +++ b/metals/src/main/scala/scala/meta/internal/implementation/Supermethods.scala @@ -7,13 +7,13 @@ import scala.meta.internal.implementation.Supermethods.formatMethodSymbolForQuic import scala.meta.internal.metals.ClientCommands import scala.meta.internal.metals.DefinitionProvider import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.metals.clients.language.MetalsQuickPickItem import scala.meta.internal.metals.clients.language.MetalsQuickPickParams import scala.meta.internal.search.SymbolHierarchyOps import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.Location diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index 8ed669053dc..effe9285495 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -26,6 +26,10 @@ import scala.meta.internal.pc.JavaPresentationCompiler import scala.meta.internal.pc.LogMessages import scala.meta.internal.pc.PcSymbolInformation import scala.meta.internal.pc.ScalaPresentationCompiler +import scala.meta.internal.telemetry +import scala.meta.internal.telemetry.ReporterContext +import scala.meta.internal.telemetry.ScalaPresentationCompilerContext +import scala.meta.internal.telemetry.TelemetryReportContext import scala.meta.internal.worksheets.WorksheetPcData import scala.meta.internal.worksheets.WorksheetProvider import scala.meta.io.AbsolutePath @@ -34,6 +38,8 @@ import scala.meta.pc.CancelToken import scala.meta.pc.HoverSignature import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompiler +import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import scala.meta.pc.SyntheticDecorationsParams @@ -1046,7 +1052,7 @@ class Compilers( ): Option[PresentationCompiler] = { val pc = JavaPresentationCompiler() Some( - configure(pc, search) + configure(pc, search, log) .newInstance( targetUri, classpath.toAbsoluteClasspath.map(_.toNIO).toSeq.asJava, @@ -1203,28 +1209,58 @@ class Compilers( sourceMapper.pcMapping(path, scalaVersion) } + private def createPresentationCompilerContext( + scalaVersion: String, + config: PresentationCompilerConfig, + options: List[String], + ): ReporterContext = + ScalaPresentationCompilerContext( + scalaVersion = scalaVersion, + options = options, + config = telemetry.conversion.PresentationCompilerConfig(config), + ) + + private def getPcConfiguration(): PresentationCompilerConfig = { + val options = + InitializationOptions.from(initializeParams).compilerOptions + config.initialConfig.compilers + .update(options) + .copy( + _symbolPrefixes = userConfig().symbolPrefixes, + isCompletionSnippetsEnabled = + initializeParams.supportsCompletionSnippets, + _isStripMarginOnTypeFormattingEnabled = + () => userConfig().enableStripMarginOnTypeFormatting, + ) + } + private def configure( pc: PresentationCompiler, search: SymbolSearch, - ): PresentationCompiler = + options: List[String], + ): PresentationCompiler = { + val pcConfig = getPcConfiguration() + val config = userConfig() + + val remoteReportContext: ReportContext = + new TelemetryReportContext( + telemetryConfiguration = () => config.telemetryConfiguration, + reporterContext = () => + createPresentationCompilerContext(pc.scalaVersion, pcConfig, options), + workspaceSanitizer = new WorkspaceSanitizer(Some(workspace.toNIO)), + telemetryClient = new telemetry.TelemetryClientImpl( + config.telemetryConfiguration.telemetryLevel + ), + ) + pc.withSearch(search) .withExecutorService(ec) .withWorkspace(workspace.toNIO) .withScheduledExecutorService(sh) .withReportsLoggerLevel(MetalsServerConfig.default.loglevel) - .withConfiguration { - val options = - InitializationOptions.from(initializeParams).compilerOptions - config.initialConfig.compilers - .update(options) - .copy( - _symbolPrefixes = userConfig().symbolPrefixes, - isCompletionSnippetsEnabled = - initializeParams.supportsCompletionSnippets, - _isStripMarginOnTypeFormattingEnabled = - () => userConfig().enableStripMarginOnTypeFormatting, - ) - } + .withConfiguration(pcConfig) + .withAdditionalReportContexts(List(remoteReportContext).asJava) + } def newCompiler( target: ScalaTarget, @@ -1266,11 +1302,13 @@ class Compilers( } val filteredOptions = plugins.filterSupportedOptions(options) - configure(pc, search) + val allOptions = log ++ filteredOptions + + configure(pc, search, allOptions) .newInstance( name, classpath.asJava, - (log ++ filteredOptions).asJava, + (allOptions).asJava, ) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala index 29a7ce5c916..e4399860d7b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala @@ -27,6 +27,7 @@ import scala.meta.internal.semanticdb.Synthetic import scala.meta.internal.semanticdb.TextDocument import scala.meta.io.AbsolutePath import scala.meta.pc.CancelToken +import scala.meta.pc.ReportContext import scala.meta.tokens.Token import ch.epfl.scala.bsp4j.BuildTargetIdentifier diff --git a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index ae28498c821..d07b32d32c5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -36,6 +36,7 @@ import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.tvp.FolderTreeViewProvider import scala.meta.internal.worksheets.WorksheetProvider import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import ch.epfl.scala.{bsp4j => b} import org.eclipse.lsp4j.Position diff --git a/metals/src/main/scala/scala/meta/internal/metals/InlayHintResolveProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/InlayHintResolveProvider.scala index 1803d1e1133..a358097546a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/InlayHintResolveProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/InlayHintResolveProvider.scala @@ -4,8 +4,10 @@ import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.pc.StandardReport import scala.meta.io.AbsolutePath import scala.meta.pc.CancelToken +import scala.meta.pc.ReportContext import com.google.gson.JsonArray import org.eclipse.lsp4j.InlayHint @@ -30,7 +32,7 @@ final class InlayHintResolveProvider( resolve(inlayHint, labelParts, path, token) case Left(error) => scribe.warn(s"Failed to resolve inlay hint: $error") - rc.unsanitized.create(report(inlayHint, path, error), ifVerbose = true) + rc.unsanitized.create(report(inlayHint, path, error), true) Future.successful(inlayHint) } } @@ -109,7 +111,7 @@ final class InlayHintResolveProvider( error: Throwable, ) = { val pos = inlayHint.getPosition() - Report( + StandardReport( "inlayHint-resolve", s"""|pos: $pos | diff --git a/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala b/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala index da38b5dc93e..76c280f5dea 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/LoggerContext.scala @@ -1,23 +1,30 @@ package scala.meta.internal.metals import java.nio.file.Path +import java.{util => ju} -import scala.meta.internal.metals.utils.TimestampedFile +import scala.meta.pc.Report +import scala.meta.pc.ReportContext +import scala.meta.pc.Reporter +import scala.meta.pc.TimestampedFile object LoggerReporter extends Reporter { override def name: String = "logger-report" - override def create(report: => Report, ifVerbose: Boolean): Option[Path] = { + override def create(report: Report, ifVerbose: Boolean): ju.Optional[Path] = { scribe.info( - s"Report ${report.name}: ${report.fullText(withIdAndSummary = false)}" + s"Report ${report.name}: ${report.fullText(false)}" ) - None + ju.Optional.empty() } - override def cleanUpOldReports(maxReportsNumber: Int): List[TimestampedFile] = - List() + override def cleanUpOldReports( + maxReportsNumber: Int + ): ju.List[TimestampedFile] = + ju.Collections.emptyList() - override def getReports(): List[TimestampedFile] = List() + override def getReports(): ju.List[TimestampedFile] = + ju.Collections.emptyList() override def deleteAll(): Unit = {} } diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index 038c246339d..88b11013142 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -40,11 +40,13 @@ import scala.meta.inputs.Input import scala.meta.internal.io.FileIO import scala.meta.internal.mtags.MtagsEnrichments import scala.meta.internal.parsing.EmptyResult +import scala.meta.internal.pc.StandardReport import scala.meta.internal.semanticdb.Scala.Descriptor import scala.meta.internal.semanticdb.Scala.Symbols import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.meta.io.RelativePath +import scala.meta.pc.ReportContext import scala.meta.trees.Origin import scala.meta.trees.Origin.Parsed @@ -716,7 +718,7 @@ object MetalsEnrichments } catch { case NonFatal(error) => reports.incognito.create( - Report( + StandardReport( "absolute-path", s"""|Uri: $value |""".stripMargin, diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index b6ce1a5bfb5..40adee80d8b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -46,6 +46,7 @@ import scala.meta.internal.io.FileIO import scala.meta.internal.metals.BuildInfo import scala.meta.internal.metals.Messages.IncompatibleBloopVersion import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.MirroredReportContext import scala.meta.internal.metals.StdReportContext import scala.meta.internal.metals.ammonite.Ammonite import scala.meta.internal.metals.callHierarchy.CallHierarchyProvider @@ -76,9 +77,12 @@ import scala.meta.internal.parsing.DocumentSymbolProvider import scala.meta.internal.parsing.FoldingRangeProvider import scala.meta.internal.parsing.TokenEditDistance import scala.meta.internal.parsing.Trees +import scala.meta.internal.pc.StandardReport import scala.meta.internal.rename.RenameProvider import scala.meta.internal.search.SymbolHierarchyOps import scala.meta.internal.semver.SemVer +import scala.meta.internal.telemetry +import scala.meta.internal.telemetry.TelemetryReportContext import scala.meta.internal.tvp._ import scala.meta.internal.worksheets.DecorationWorksheetPublisher import scala.meta.internal.worksheets.WorksheetProvider @@ -87,6 +91,7 @@ import scala.meta.io.AbsolutePath import scala.meta.metals.lsp.TextDocumentService import scala.meta.parsers.ParseException import scala.meta.pc.CancelToken +import scala.meta.pc.ReportContext import scala.meta.tokenizers.TokenizeException import ch.epfl.scala.bsp4j.CompileReport @@ -146,6 +151,8 @@ class MetalsLspService( ThreadPools.discardRejectedRunnables("MetalsLanguageServer.ec", ec) def getVisibleName: String = folderVisibleName.getOrElse(folder.toString()) + def getTelemetryConfiguration: TelemetryConfiguration = + userConfig.telemetryConfiguration private val cancelables = new MutableCancelable() val isCancelled = new AtomicBoolean(false) @@ -177,8 +184,7 @@ class MetalsLspService( ) val tables: Tables = register(new Tables(folder, time)) - - implicit val reports: StdReportContext = new StdReportContext( + val localFileReports: StdReportContext = new StdReportContext( folder.toNIO, _.flatMap { uri => for { @@ -190,8 +196,20 @@ class MetalsLspService( ReportLevel.fromString(MetalsServerConfig.default.loglevel), ) + private val remoteTelemetryReports = new TelemetryReportContext( + telemetryConfiguration = () => userConfig.telemetryConfiguration, + reporterContext = () => createTelemetryReporterContext, + workspaceSanitizer = new WorkspaceSanitizer(Some(folder.toNIO)), + telemetryClient = new telemetry.TelemetryClientImpl( + userConfig.telemetryConfiguration.telemetryLevel + ), + ) + + implicit val reports: ReportContext = + new MirroredReportContext(localFileReports, remoteTelemetryReports) + val folderReportsZippper: FolderReportsZippper = - FolderReportsZippper(doctor.getTargetsInfoForReports, reports) + FolderReportsZippper(doctor.getTargetsInfoForReports, localFileReports) private val buildTools: BuildTools = new BuildTools( folder, @@ -212,8 +230,7 @@ class MetalsLspService( new AtomicReference[b.BuildTargetIdentifier]() private val definitionIndex = newSymbolIndex() private val symbolDocs = new Docstrings(definitionIndex) - var bspSession: Option[BspSession] = - Option.empty[BspSession] + var bspSession: Option[BspSession] = Option.empty[BspSession] private val savedFiles = new ActiveFiles(time) private val recentlyOpenedFiles = new ActiveFiles(time) val isImportInProcess = new AtomicBoolean(false) @@ -963,7 +980,10 @@ class MetalsLspService( } } - if (userConfig.symbolPrefixes != old.symbolPrefixes) { + if ( + userConfig.symbolPrefixes != old.symbolPrefixes || + userConfig.telemetryConfiguration != old.telemetryConfiguration + ) { compilers.restartAll() } @@ -1264,7 +1284,7 @@ class MetalsLspService( val (bloopReportDelete, otherDeleteEvents) = deleteEvents.partition( _.getUri().toAbsolutePath.toNIO - .startsWith(reports.bloop.maybeReportsDir) + .startsWith(localFileReports.bloop.maybeReportsDir) ) if (bloopReportDelete.nonEmpty) connectionBspStatus.onReportsUpdate() otherDeleteEvents.map(_.getUri().toAbsolutePath).foreach(onDelete) @@ -2654,7 +2674,7 @@ class MetalsLspService( scribe.error(s"issues while parsing: ${e.path}", e.getCause) case e: IndexingExceptions.InvalidSymbolException => reports.incognito.create( - Report( + StandardReport( "invalid-symbol", s"""Symbol: ${e.symbol}""".stripMargin, e, @@ -2848,4 +2868,16 @@ class MetalsLspService( def runDoctorCheck(): Unit = doctor.check(headDoctor) + private def createTelemetryReporterContext(): telemetry.ReporterContext = + telemetry.MetalsLspContext( + metalsVersion = BuildInfo.metalsVersion, + userConfig = telemetry.conversion.UserConfiguration(userConfig), + serverConfig = telemetry.conversion.MetalsServerConfig( + serverInputs.initialServerConfig + ), + clientInfo = + telemetry.conversion.MetalsClientInfo(initializeParams.getClientInfo()), + buildServerConnections = bspSession.toList + .flatMap(telemetry.conversion.BuildServerConnections(_)), + ) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala index ce1364f499c..5ef212ad127 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala @@ -10,6 +10,7 @@ import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.mtags.Mtags import scala.meta.io.AbsolutePath import scala.meta.pc.ParentSymbols +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearchVisitor diff --git a/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala index 50fe2a65667..5d26465e266 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ReferenceProvider.scala @@ -17,6 +17,7 @@ import scala.meta.internal.mtags.Semanticdbs import scala.meta.internal.mtags.Symbol import scala.meta.internal.parsing.TokenEditDistance import scala.meta.internal.parsing.Trees +import scala.meta.internal.pc.StandardReport import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.SymbolOccurrence @@ -25,6 +26,7 @@ import scala.meta.internal.semanticdb.TextDocument import scala.meta.internal.semanticdb.TextDocuments import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import ch.epfl.scala.bsp4j.BuildTargetIdentifier import com.google.common.hash.BloomFilter @@ -174,7 +176,7 @@ final class ReferenceProvider( s"No references found, index size ${index.size}\n" + fileInIndex ) report.unsanitized.create( - Report( + StandardReport( "empty-references", index .map { case (path, entry) => diff --git a/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala b/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala index e45e7596984..0942b6cc36d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala @@ -12,6 +12,7 @@ import scala.meta.internal.mtags.OnDemandSymbolIndex import scala.meta.internal.parsing.Trees import scala.meta.io.AbsolutePath import scala.meta.pc.ParentSymbols +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearch.Result diff --git a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala index 62ba6667c71..cedc58e159a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala @@ -57,6 +57,8 @@ case class UserConfiguration( automaticImportBuild: AutoImportBuildKind = AutoImportBuildKind.Off, scalaCliLauncher: Option[String] = None, defaultBspToBuildTool: Boolean = false, + telemetryConfiguration: TelemetryConfiguration = + TelemetryConfiguration.default, ) { def shouldAutoImportNewProject: Boolean = @@ -245,8 +247,8 @@ object UserConfiguration { "false", "false", "Should display implicit parameter at usage sites", - """|When this option is enabled, each method that has implicit arguments has them - |displayed either as additional decorations if they are supported by the editor or + """|When this option is enabled, each method that has implicit arguments has them + |displayed either as additional decorations if they are supported by the editor or |shown in the hover. |""".stripMargin, ), @@ -255,8 +257,8 @@ object UserConfiguration { "false", "false", "Should display implicit conversion at usage sites", - """|When this option is enabled, each place where an implicit method or class is used has it - |displayed either as additional decorations if they are supported by the editor or + """|When this option is enabled, each place where an implicit method or class is used has it + |displayed either as additional decorations if they are supported by the editor or |shown in the hover. |""".stripMargin, ), @@ -283,7 +285,7 @@ object UserConfiguration { BuildInfo.scala3, BuildInfo.scala3, "Default fallback Scala version", - """|The Scala compiler version that is used as the default or fallback in case a file + """|The Scala compiler version that is used as the default or fallback in case a file |doesn't belong to any build target or the specified Scala version isn't supported by Metals. |This applies to standalone Scala files, worksheets, and Ammonite scripts. """.stripMargin, @@ -344,13 +346,43 @@ object UserConfiguration { |will make the logs contain all the possible debugging information including |about incremental compilation in Zinc.""".stripMargin, ), + UserConfigurationOption( + "telemetry-level", + TelemetryLevel.default.textValue, + TelemetryLevel.All.textValue, + "Scope of reported telemetry data", + s"""Control what kind of telemetry events can be send to maintainers of Metals. + |With `${TelemetryLevel.Off.textValue}` no telemetry data would be send. + |Minimal recommended level is `${TelemetryLevel.Crash.textValue}` which would collect diagnostic information when Metals components would crash or fail unexpectedly, allowing to understand why the problem occoured. + |The highest telemtetry level `${TelemetryLevel.All.textValue}` allows to collect all information including how features are used to help us priortize future improvements." + |Defaults to `${TelemetryLevel.default.textValue}`. + |""".stripMargin, + ), + UserConfigurationOption( + "auto-import-build", + "off", + "all", + "Import build when changes detected without prompting", + """|Automatically import builds rather than prompting the user to choose. "initial" will + |only automatically import a build when a project is first opened, "all" will automate + |build imports after subsequent changes as well.""".stripMargin, + ), + UserConfigurationOption( + "default-bsp-to-build-tool", + "false", + "true", + "Default to using build tool as your build server.", + """|If your build tool can also serve as a build server, + |default to using it instead of Bloop. + |""".stripMargin, + ), UserConfigurationOption( "auto-import-build", "off", "all", "Import build when changes detected without prompting", - """|Automatically import builds rather than prompting the user to choose. "initial" will - |only automatically import a build when a project is first opened, "all" will automate + """|Automatically import builds rather than prompting the user to choose. "initial" will + |only automatically import a build when a project is first opened, "all" will automate |build imports after subsequent changes as well.""".stripMargin, ), UserConfigurationOption( @@ -370,6 +402,7 @@ object UserConfiguration { properties: Properties = System.getProperties, ): Either[List[String], UserConfiguration] = { val errors = ListBuffer.empty[String] + scribe.info(json.toString) def getKey[A]( key: String, @@ -588,40 +621,53 @@ object UserConfiguration { val defaultBspToBuildTool = getBooleanKey("default-bsp-to-build-tool").getOrElse(false) + val telemetryConfiguration = { + val telemetryLevel = + getStringKey("telemetry-level") + .map(TelemetryLevel.fromString) + .getOrElse(TelemetryLevel.default) + val includeCodeSnippets = + getBooleanKey("include-code-snippets") + .getOrElse(false) + + TelemetryConfiguration(telemetryLevel, includeCodeSnippets) + } + if (errors.isEmpty) { Right( UserConfiguration( - javaHome, - sbtScript, - gradleScript, - mavenScript, - millScript, - scalafmtConfigPath, - scalafixConfigPath, - symbolPrefixes, - worksheetScreenWidth, - worksheetCancelTimeout, - bloopSbtAlreadyInstalled, - bloopVersion, - bloopJvmProperties, - ammoniteProperties, - superMethodLensesEnabled, - showInferredType, - showImplicitArguments, - showImplicitConversionsAndClasses, - enableStripMarginOnTypeFormatting, - enableIndentOnPaste, - enableSemanticHighlighting, - excludedPackages, - defaultScalaVersion, - disableTestCodeLenses, - javaFormatConfig, - scalafixRulesDependencies, - customProjectRoot, - verboseCompilation, - autoImportBuilds, - scalaCliLauncher, - defaultBspToBuildTool, + javaHome = javaHome, + sbtScript = sbtScript, + gradleScript = gradleScript, + mavenScript = mavenScript, + millScript = millScript, + scalafmtConfigPath = scalafmtConfigPath, + scalafixConfigPath = scalafixConfigPath, + symbolPrefixes = symbolPrefixes, + worksheetScreenWidth = worksheetScreenWidth, + worksheetCancelTimeout = worksheetCancelTimeout, + bloopSbtAlreadyInstalled = bloopSbtAlreadyInstalled, + bloopVersion = bloopVersion, + bloopJvmProperties = bloopJvmProperties, + ammoniteJvmProperties = ammoniteProperties, + superMethodLensesEnabled = superMethodLensesEnabled, + showInferredType = showInferredType, + showImplicitArguments = showImplicitArguments, + showImplicitConversionsAndClasses = showImplicitConversionsAndClasses, + enableStripMarginOnTypeFormatting = enableStripMarginOnTypeFormatting, + enableIndentOnPaste = enableIndentOnPaste, + enableSemanticHighlighting = enableSemanticHighlighting, + excludedPackages = excludedPackages, + fallbackScalaVersion = defaultScalaVersion, + testUserInterface = disableTestCodeLenses, + javaFormatConfig = javaFormatConfig, + scalafixRulesDependencies = scalafixRulesDependencies, + customProjectRoot = customProjectRoot, + verboseCompilation = verboseCompilation, + telemetryConfiguration = telemetryConfiguration, + scalaCliLauncher = scalaCliLauncher, + automaticImportBuild = autoImportBuilds, + defaultBspToBuildTool = defaultBspToBuildTool, ) ) } else { diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala index d869ef646ee..748bffa5d6c 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -43,6 +43,7 @@ import scala.meta.internal.tvp.TreeViewVisibilityDidChangeParams import scala.meta.io.AbsolutePath import scala.meta.metals.lsp.ScalaLspService import scala.meta.pc.DisplayableException +import scala.meta.pc.ReportContext import com.google.gson.Gson import com.google.gson.JsonPrimitive diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSearchVisitor.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSearchVisitor.scala index 8652ed785f7..06ff8691b39 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSearchVisitor.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSearchVisitor.scala @@ -13,6 +13,7 @@ import scala.meta.internal.semanticdb.Scala.Descriptor import scala.meta.internal.semanticdb.Scala.DescriptorParser import scala.meta.internal.semanticdb.Scala.Symbols import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearchVisitor import org.eclipse.lsp4j.SymbolKind diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala index c6ce15437c1..69d9b08f7c6 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceSymbolProvider.scala @@ -12,6 +12,7 @@ import scala.meta.internal.mtags.SymbolDefinition import scala.meta.internal.pc.InterruptException import scala.meta.io.AbsolutePath import scala.meta.pc.CancelToken +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearchVisitor diff --git a/metals/src/main/scala/scala/meta/internal/metals/ZipReportsProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/ZipReportsProvider.scala index 4354a609f0c..d0776221a51 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ZipReportsProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ZipReportsProvider.scala @@ -4,6 +4,8 @@ import java.nio.file.Files import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream +import scala.jdk.CollectionConverters._ + import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.io.AbsolutePath @@ -71,8 +73,8 @@ object ZipReportsProvider { for { (folder, id) <- folders.zipWithIndex - reportsProvider <- folder.reportContext.allToZip - report <- reportsProvider.getReports() + reportsProvider <- folder.reportContext.allToZip.asScala + report <- reportsProvider.getReports().asScala } { val zipEntry = new ZipEntry(s"$id-${report.name}") zipOut.putNextEntry(zipEntry) diff --git a/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala b/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala index 5f9fb847230..3b32e24b2e9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala @@ -32,16 +32,16 @@ import scala.meta.internal.metals.Messages.CheckDoctor import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.MtagsResolver import scala.meta.internal.metals.PopupChoiceReset -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.ReportFileName import scala.meta.internal.metals.ScalaTarget import scala.meta.internal.metals.ServerCommands import scala.meta.internal.metals.StdReportContext import scala.meta.internal.metals.Tables import scala.meta.internal.metals.clients.language.MetalsLanguageClient -import scala.meta.internal.metals.utils.TimestampedFile +import scala.meta.internal.pc.StandardReport import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext +import scala.meta.pc.TimestampedFile import ch.epfl.scala.bsp4j.BuildTargetIdentifier import org.eclipse.{lsp4j => l} @@ -241,8 +241,8 @@ final class Doctor( private def getErrorReports(): List[ErrorReportInfo] = for { - provider <- rc.all - report <- provider.getReports() + provider <- rc.all().asScala.toList + report <- provider.getReports().asScala.toList } yield { val (name, buildTarget) = ReportFileName.getReportNameAndBuildTarget(report) @@ -665,7 +665,7 @@ object Doctor { text.replace(StdReportContext.WORKSPACE_STR, root.toString()) for { lines <- Try(Files.readAllLines(file.toPath).asScala.toList).toOption - index = lines.lastIndexWhere(_.startsWith(Report.summaryTitle)) + index = lines.lastIndexWhere(_.startsWith(StandardReport.summaryTitle)) if index >= 0 } yield lines .drop(index + 1) diff --git a/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala b/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala index 56803003e9c..2dc4c057ddc 100644 --- a/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala +++ b/metals/src/main/scala/scala/meta/internal/parsing/Trees.scala @@ -7,13 +7,13 @@ import scala.meta._ import scala.meta.inputs.Position import scala.meta.internal.metals.Buffers import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.ScalaVersionSelector +import scala.meta.internal.pc.StandardReport import scala.meta.io.AbsolutePath import scala.meta.parsers.Parse import scala.meta.parsers.ParseException import scala.meta.parsers.Parsed +import scala.meta.pc.ReportContext import scala.meta.tokens.Tokens import org.eclipse.lsp4j.Diagnostic @@ -154,7 +154,7 @@ final class Trees( // if the parsers breaks we should not throw the exception further case _: StackOverflowError => val newPathCopy = reports.unsanitized.create( - Report( + StandardReport( s"stackoverflow_${path.filename}", text, s"Stack overflow in ${path.filename}", diff --git a/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala b/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala index 94db85f3934..2bb0597e591 100644 --- a/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/rename/RenameProvider.scala @@ -16,7 +16,6 @@ import scala.meta.internal.metals.Compilers import scala.meta.internal.metals.DefinitionProvider import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.ReferenceProvider -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.TextEdits import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.parsing.Trees @@ -30,6 +29,7 @@ import scala.meta.internal.semanticdb.TextDocument import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.meta.pc.CancelToken +import scala.meta.pc.ReportContext import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.MessageParams diff --git a/metals/src/main/scala/scala/meta/internal/telemetry/TelemetryClient.scala b/metals/src/main/scala/scala/meta/internal/telemetry/TelemetryClient.scala new file mode 100644 index 00000000000..ab46a738e9d --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/telemetry/TelemetryClient.scala @@ -0,0 +1,74 @@ +package scala.meta.internal.telemetry + +import scala.meta.internal.metals.TelemetryLevel + +import com.google.common.util.concurrent.RateLimiter +import sttp.client3._ +import sttp.model.Uri +import sttp.tapir.client.sttp.SttpClientInterpreter + +object TelemetryClient { + + case class Config(serverHost: Uri) + object Config { + // private final val DefaultTelemetryEndpoint = + // "https://scala3.westeurope.cloudapp.azure.com/telemetry" + private final val DefaultTelemetryEndpoint = uri"http://localhost:8081" + val default: Config = Config(DefaultTelemetryEndpoint) + } + + protected[telemetry] val interpreter: SttpClientInterpreter = + SttpClientInterpreter() + +} + +trait TelemetryClient { + val telemetryLevel: TelemetryLevel + + protected val sendErrorReportImpl: ErrorReport => Unit + protected val sendCrashReportImpl: CrashReport => Unit + + val sendErrorReport: ErrorReport => Unit = report => + if (telemetryLevel.sendErrors) { + scribe.debug("Sending remote error report.") + sendErrorReportImpl(report) + } + + val sendCrashReport: CrashReport => Unit = report => { + if (telemetryLevel.sendCrashes) { + scribe.debug("Sending remote crash report.") + sendCrashReportImpl(report) + } + } + +} + +class TelemetryClientImpl( + val telemetryLevel: TelemetryLevel, + config: TelemetryClient.Config = TelemetryClient.Config.default, +) extends TelemetryClient { + import TelemetryClient._ + private val backend = HttpClientFutureBackend() + private val rateLimiter = RateLimiter.create(1.0 / 5.0) + + protected val sendErrorReportImpl: ErrorReport => Unit = report => + if (rateLimiter.tryAcquire()) { + interpreter + .toClient( + TelemetryEndpoints.sendErrorReport, + baseUri = Some(config.serverHost), + backend = backend, + ) + .apply(report) + } else scribe.debug("Report not send because of quota.") + + protected val sendCrashReportImpl: CrashReport => Unit = report => + interpreter + .toClient( + TelemetryEndpoints.sendCrashReport, + baseUri = Some(config.serverHost), + backend = backend, + ) + .apply(report) + +} diff --git a/metals/src/main/scala/scala/meta/internal/telemetry/TelemetryReportContext.scala b/metals/src/main/scala/scala/meta/internal/telemetry/TelemetryReportContext.scala new file mode 100644 index 00000000000..4f114538057 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/telemetry/TelemetryReportContext.scala @@ -0,0 +1,113 @@ +package scala.meta.internal.telemetry + +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicReference +import java.{util => ju} + +import scala.meta.internal.metals.EmptyReporter +import scala.meta.internal.metals.TelemetryConfiguration +import scala.meta.internal.metals.WorkspaceSanitizer +import scala.meta.internal.mtags.CommonMtagsEnrichments.XtensionOptionalJava +import scala.meta.internal.telemetry +import scala.meta.pc.Report +import scala.meta.pc.ReportContext +import scala.meta.pc.Reporter +import scala.meta.pc.TimestampedFile + +import com.google.common.collect.EvictingQueue + +/** + * A remote reporter sending reports to telemetry server aggregating the results. Operates in a best-effort manner. Created reporter does never reutrn any values. + * + * @param telemetryServerEndpoint + * @param getReporterContext Constructor of reporter context metadata containg informations about user/server configuration of components + */ +class TelemetryReportContext( + telemetryConfiguration: () => TelemetryConfiguration, + reporterContext: () => telemetry.ReporterContext, + workspaceSanitizer: WorkspaceSanitizer, + telemetryClient: TelemetryClient, +) extends ReportContext { + + val telemetryConfig0: TelemetryConfiguration = telemetryConfiguration() + scribe.info(s"Telemetry enabled with level: $telemetryConfig0") + + // Don't send reports with fragile user data - sources etc + override lazy val unsanitized: Reporter = + if ( + telemetryConfig0.telemetryLevel.enabled && telemetryConfig0.includeCodeSnippet + ) + reporter("unsanitized") + else EmptyReporter + override lazy val incognito: Reporter = + if (telemetryConfig0.telemetryLevel.enabled) reporter("incognito") + else EmptyReporter + override lazy val bloop: Reporter = + if (telemetryConfig0.telemetryLevel.enabled) reporter("bloop") + else EmptyReporter + + private def reporter(name: String) = new TelemetryReporter( + name = name, + client = telemetryClient, + reporterContext = reporterContext, + sanitizers = workspaceSanitizer, + ) +} + +private class TelemetryReporter( + override val name: String, + client: TelemetryClient, + reporterContext: () => telemetry.ReporterContext, + sanitizers: WorkspaceSanitizer, +) extends Reporter { + + val previousTraces: AtomicReference[EvictingQueue[ExceptionSummary]] = + new AtomicReference(EvictingQueue.create(10)) + + def alreadyReported(report: ErrorReport): Boolean = { + report.error.exists(previousTraces.get.contains) + } + + override def getReports(): ju.List[TimestampedFile] = + ju.Collections.emptyList() + + override def cleanUpOldReports( + maxReportsNumber: Int + ): ju.List[TimestampedFile] = + ju.Collections.emptyList() + + override def deleteAll(): Unit = () + + override def sanitize(message: String): String = + sanitizers(message) + + private def createSanitizedReport(report: Report) = { + new telemetry.ErrorReport( + name = report.name, + reporterName = name, + reporterContext = reporterContext(), + id = report.id.asScala, + text = report.text, + error = report.error + .map(telemetry.ExceptionSummary.from(_, sanitize(_))) + .asScala, + ) + } + + override def create( + unsanitizedReport: Report, + ifVerbose: Boolean, + ): ju.Optional[Path] = { + val report = createSanitizedReport(unsanitizedReport) + if (!alreadyReported(report)) { + report.error.foreach(a => previousTraces.get.add(a)) + client.sendErrorReport(report) + } else { + scribe.debug( + "Skipped reporting remotely duplicated report, reportId=" + + unsanitizedReport.id.orElse("null") + ) + } + ju.Optional.empty() + } +} diff --git a/metals/src/main/scala/scala/meta/internal/telemetry/conversion/package.scala b/metals/src/main/scala/scala/meta/internal/telemetry/conversion/package.scala new file mode 100644 index 00000000000..8f13a7e2408 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/telemetry/conversion/package.scala @@ -0,0 +1,95 @@ +package scala.meta.internal.telemetry + +import scala.collection.JavaConverters._ +import scala.jdk.OptionConverters._ + +import scala.meta.internal.bsp +import scala.meta.internal.metals +import scala.meta.internal.telemetry +import scala.meta.pc + +import org.eclipse.lsp4j + +package object conversion { + def UserConfiguration( + config: metals.UserConfiguration + ): MetalsUserConfiguration = + MetalsUserConfiguration( + symbolPrefixes = config.symbolPrefixes, + bloopSbtAlreadyInstalled = config.bloopSbtAlreadyInstalled, + bloopVersion = config.bloopVersion, + bloopJvmProperties = config.bloopJvmProperties.toList.flatten, + ammoniteJvmProperties = config.ammoniteJvmProperties.toList.flatten, + superMethodLensesEnabled = config.superMethodLensesEnabled, + showInferredType = config.showInferredType, + showImplicitArguments = config.showImplicitArguments, + showImplicitConversionsAndClasses = + config.showImplicitConversionsAndClasses, + enableStripMarginOnTypeFormatting = + config.enableStripMarginOnTypeFormatting, + enableIndentOnPaste = config.enableIndentOnPaste, + enableSemanticHighlighting = config.enableSemanticHighlighting, + excludedPackages = config.excludedPackages.toList.flatten, + fallbackScalaVersion = config.fallbackScalaVersion, + testUserInterface = TestUserInterfaceKind(config.testUserInterface), + ) + + def PresentationCompilerConfig( + config: pc.PresentationCompilerConfig + ): telemetry.PresentationCompilerConfig = + telemetry.PresentationCompilerConfig( + symbolPrefixes = config.symbolPrefixes().asScala.toMap, + completionCommand = config.completionCommand().asScala, + parameterHintsCommand = config.parameterHintsCommand().asScala, + overrideDefFormat = config.overrideDefFormat.name(), + isDefaultSymbolPrefixes = config.isDefaultSymbolPrefixes(), + isCompletionItemDetailEnabled = config.isCompletionItemDetailEnabled(), + isStripMarginOnTypeFormattingEnabled = + config.isStripMarginOnTypeFormattingEnabled(), + isCompletionItemDocumentationEnabled = + config.isCompletionItemDocumentationEnabled(), + isHoverDocumentationEnabled = config.isHoverDocumentationEnabled(), + snippetAutoIndent = config.snippetAutoIndent(), + isSignatureHelpDocumentationEnabled = + config.isSignatureHelpDocumentationEnabled(), + isCompletionSnippetsEnabled = config.isCompletionSnippetsEnabled(), + semanticdbCompilerOptions = + config.semanticdbCompilerOptions.asScala.toList, + ) + + def BuildServerConnections( + session: bsp.BspSession + ): List[BuildServerConnection] = { + def convert(conn: metals.BuildServerConnection, isMain: Boolean) = + BuildServerConnection(conn.name, conn.version, isMain) + + convert(session.main, isMain = true) :: + session.meta.map(convert(_, isMain = false)) + } + + def TestUserInterfaceKind(kind: metals.TestUserInterfaceKind): String = + kind match { + case metals.TestUserInterfaceKind.CodeLenses => "CodeLenses" + case metals.TestUserInterfaceKind.TestExplorer => "TestExplorer" + } + + def MetalsServerConfig( + config: metals.MetalsServerConfig + ): MetalsServerConfiguration = + MetalsServerConfiguration( + config.executeClientCommand.value, + config.snippetAutoIndent, + config.isHttpEnabled, + config.isInputBoxEnabled, + config.askToReconnect, + config.allowMultilineStringFormatting, + PresentationCompilerConfig(config.compilers), + ) + + def MetalsClientInfo(info: lsp4j.ClientInfo): telemetry.MetalsClientInfo = + telemetry.MetalsClientInfo( + Option(info.getName()), + Option(info.getVersion()), + ) + +} diff --git a/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala b/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala index 892752ddfd4..07966e789fc 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala @@ -11,7 +11,6 @@ import scala.meta.internal.io.FileIO import scala.meta.internal.metals.Buffers import scala.meta.internal.metals.BuildTargets import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.Time import scala.meta.internal.metals.Timer import scala.meta.internal.mtags.JavaMtags @@ -22,6 +21,7 @@ import scala.meta.internal.mtags.SymbolDefinition import scala.meta.internal.parsing.Trees import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import scala.meta.trees.Origin class IndexedSymbols( diff --git a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala index c4ee8563508..88f63892171 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala @@ -11,7 +11,6 @@ import scala.collection.concurrent.TrieMap import scala.meta.Dialect import scala.meta.dialects import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals._ import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.mtags.GlobalSymbolIndex @@ -21,6 +20,7 @@ import scala.meta.internal.parsing.Trees import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.semanticdb.SymbolOccurrence import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import ch.epfl.scala.bsp4j.BuildTarget import ch.epfl.scala.bsp4j.BuildTargetIdentifier diff --git a/metals/src/main/scala/scala/meta/metals/Main.scala b/metals/src/main/scala/scala/meta/metals/Main.scala index a38b36922eb..3acf41950b2 100644 --- a/metals/src/main/scala/scala/meta/metals/Main.scala +++ b/metals/src/main/scala/scala/meta/metals/Main.scala @@ -10,6 +10,9 @@ import scala.meta.internal.metals.ScalaVersions import scala.meta.internal.metals.Trace import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.metals.logging.MetalsLogger +import scala.meta.internal.telemetry.CrashReport +import scala.meta.internal.telemetry.ExceptionSummary +import scala.meta.internal.telemetry.TelemetryClientImpl import org.eclipse.lsp4j.jsonrpc.Launcher @@ -63,6 +66,7 @@ object Main { launcher.startListening().get() } catch { case NonFatal(e) => + trySendingCrashReport(e, server) e.printStackTrace(systemOut) sys.exit(1) } finally { @@ -74,4 +78,25 @@ object Main { } } + private def trySendingCrashReport( + error: Throwable, + server: MetalsLanguageServer, + ): Unit = try { + val telemetry = new TelemetryClientImpl(server.getTelemetryLevel()) + telemetry.sendCrashReport( + CrashReport( + // We strip message as it can contain personal data such as path to the file. + // It may be changed in the future depending on how usefull the reports will + // be with ommitted message. + error = ExceptionSummary + .from(error, sanitize = _ => error.getClass().getName()), + componentName = this.getClass().getName(), + componentVersion = Some(BuildInfo.metalsVersion), + ) + ) + } catch { + case err: Throwable => + scribe.error(s"Failed to send crash report, $err") + } + } diff --git a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala index 9cef353b2fb..5e89293c342 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala @@ -17,6 +17,7 @@ import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.MetalsServerInputs import scala.meta.internal.metals.MutableCancelable import scala.meta.internal.metals.StdReportContext +import scala.meta.internal.metals.TelemetryLevel import scala.meta.internal.metals.ThreadPools import scala.meta.internal.metals.WorkspaceLspService import scala.meta.internal.metals.clients.language.MetalsLanguageClient @@ -283,4 +284,29 @@ class MetalsLanguageServer( case _ => throw new IllegalStateException("Server is not initialized") } + /** + * Telemetry level used only for initialisation purpose. + * It will return the minimal level set, within all workspaces e.g. + * - WorkspaceA - telemetry off, + * - WorkspaceB - telemetry full, + * the method will return telemetry off + */ + private[metals] def getTelemetryLevel(): TelemetryLevel = { + def lowestSettingWithinWorkspaces( + service: WorkspaceLspService + ): TelemetryLevel = + service.workspaceFolders.getFolderServices + .map(_.getTelemetryConfiguration) + .minBy(_.telemetryLevel) + .telemetryLevel + + serverState.get() match { + case ServerState.Initialized(service) => + lowestSettingWithinWorkspaces(service) + case ServerState.ShuttingDown(service) => + lowestSettingWithinWorkspaces(service) + case _ => TelemetryLevel.default + } + } + } diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java index 734cfc26e91..44ecf4388ac 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java @@ -227,6 +227,13 @@ public PresentationCompiler withReportsLoggerLevel(String level) { return this; }; + /** + * Set remote report context that will be added on top of standard, local reporting system. + */ + public PresentationCompiler withAdditionalReportContexts(List reportContexts) { + return this; + }; + /** * Set build target name. */ diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/Report.java b/mtags-interfaces/src/main/java/scala/meta/pc/Report.java new file mode 100644 index 00000000000..5947d0dce6e --- /dev/null +++ b/mtags-interfaces/src/main/java/scala/meta/pc/Report.java @@ -0,0 +1,16 @@ +package scala.meta.pc; + +import java.util.Optional; +import java.lang.StringBuilder; + +public interface Report { + String name(); + String text(); + String shortSummary(); + Optional path(); + Optional id(); + Optional error(); + + public Report extend(String moreInfo); + public String fullText(boolean withIdAndSummary); +} diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/ReportContext.java b/mtags-interfaces/src/main/java/scala/meta/pc/ReportContext.java new file mode 100644 index 00000000000..9b4525caa0b --- /dev/null +++ b/mtags-interfaces/src/main/java/scala/meta/pc/ReportContext.java @@ -0,0 +1,24 @@ +package scala.meta.pc; + +import java.util.List; +import java.util.Arrays; +import scala.meta.pc.Reporter; + +public interface ReportContext { + Reporter unsanitized(); + Reporter incognito(); + Reporter bloop(); + + default List all() { + return Arrays.asList(unsanitized(), incognito(), bloop()); + }; + default List allToZip() { + return Arrays.asList(incognito(), bloop()); + }; + default void cleanUpOldReports(int maxReportsNumber) { + all().stream().forEach(report -> report.cleanUpOldReports(maxReportsNumber)); + }; + default void deleteAll() { + all().stream().forEach(report -> report.deleteAll()); + }; +} diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/Reporter.java b/mtags-interfaces/src/main/java/scala/meta/pc/Reporter.java new file mode 100644 index 00000000000..5161d3e695b --- /dev/null +++ b/mtags-interfaces/src/main/java/scala/meta/pc/Reporter.java @@ -0,0 +1,15 @@ +package scala.meta.pc; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +public interface Reporter { + String name(); + Optional create(Report report, boolean ifVerbose); + default Optional create(Report report) { return create(report, false); }; + List cleanUpOldReports(int maxReportsNumber); + List getReports(); + void deleteAll(); + default String sanitize(String message) { return message; } +} diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/TimestampedFile.java b/mtags-interfaces/src/main/java/scala/meta/pc/TimestampedFile.java new file mode 100644 index 00000000000..26ec36da831 --- /dev/null +++ b/mtags-interfaces/src/main/java/scala/meta/pc/TimestampedFile.java @@ -0,0 +1,22 @@ +package scala.meta.pc; + +import java.io.File; +import java.nio.file.Path; + +public class TimestampedFile { + public File file; + public long timestamp; + + public TimestampedFile(File file, long timestamp) { + this.file = file; + this.timestamp = timestamp; + } + + public Path toPath() { + return file.toPath(); + } + + public String name() { + return file.getName(); + } +} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/ProvidedReportContexts.scala similarity index 53% rename from mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala rename to mtags-shared/src/main/scala/scala/meta/internal/metals/ProvidedReportContexts.scala index 0f8131a1c29..0dc3668f710 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportContext.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/ProvidedReportContexts.scala @@ -5,36 +5,18 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference +import java.{util => ju} -import scala.util.Try import scala.util.matching.Regex +import scala.meta.internal.jdk.CollectionConverters._ import scala.meta.internal.metals.utils.LimitedFilesManager -import scala.meta.internal.metals.utils.TimestampedFile import scala.meta.internal.mtags.CommonMtagsEnrichments._ - -trait ReportContext { - def unsanitized: Reporter - def incognito: Reporter - def bloop: Reporter - def all: List[Reporter] = List(unsanitized, incognito, bloop) - def allToZip: List[Reporter] = List(incognito, bloop) - def cleanUpOldReports( - maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS - ): Unit = all.foreach(_.cleanUpOldReports(maxReportsNumber)) - def deleteAll(): Unit = all.foreach(_.deleteAll()) -} - -trait Reporter { - def name: String - def create(report: => Report, ifVerbose: Boolean = false): Option[Path] - def cleanUpOldReports( - maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS - ): List[TimestampedFile] - def getReports(): List[TimestampedFile] - def deleteAll(): Unit - def sanitize(message: String) = message -} +import scala.meta.internal.pc.StandardReport +import scala.meta.pc.Report +import scala.meta.pc.ReportContext +import scala.meta.pc.Reporter +import scala.meta.pc.TimestampedFile class StdReportContext( workspace: Path, @@ -70,12 +52,11 @@ class StdReportContext( override def cleanUpOldReports( maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS - ): Unit = { - all.foreach(_.cleanUpOldReports(maxReportsNumber)) - } + ): Unit = + super.cleanUpOldReports(maxReportsNumber) override def deleteAll(): Unit = { - all.foreach(_.deleteAll()) + all.forEach(_.deleteAll()) val zipFile = reportsDir.resolve(StdReportContext.ZIP_FILE_NAME) if (Files.exists(zipFile)) Files.delete(zipFile) } @@ -88,6 +69,9 @@ class StdReporter( level: ReportLevel, override val name: String ) extends Reporter { + private val sanitizer: ReportSanitizer = new WorkspaceSanitizer( + Some(workspace) + ) val maybeReportsDir: Path = workspace.resolve(pathToReports).resolve(name) private lazy val reportsDir = maybeReportsDir.createDirectories() @@ -99,18 +83,16 @@ class StdReporter( ".md" ) - private lazy val userHome = Option(System.getProperty("user.home")) - private val initialized = new AtomicBoolean(false) private val reported = new AtomicReference(Map[String, Path]()) def readInIds(): Unit = { - val reports = getReports().flatMap { report => + val reports = getReports().asScala.flatMap { report => val lines = Files.readAllLines(report.file.toPath()) if (lines.size() > 0) { lines.get(0) match { - case id if id.startsWith(Report.idPrefix) => - Some((id.stripPrefix(Report.idPrefix) -> report.toPath)) + case id if id.startsWith(StandardReport.idPrefix) => + Some((id.stripPrefix(StandardReport.idPrefix) -> report.toPath)) case _ => None } } else None @@ -119,61 +101,56 @@ class StdReporter( } override def create( - report: => Report, + report: Report, ifVerbose: Boolean = false - ): Option[Path] = - if (ifVerbose && !level.isVerbose) None + ): ju.Optional[Path] = + if (ifVerbose && !level.isVerbose) ju.Optional.empty() else { if (initialized.compareAndSet(false, true)) { readInIds() } - val sanitizedId = report.id.map(sanitize) + val sanitizedId: Option[String] = report.id.asScala.map(sanitize(_)) val path = reportPath(report) - val optDuplicate = - for { - id <- sanitizedId - reportedMap = reported.getAndUpdate(map => - if (map.contains(id)) map else map + (id -> path) - ) - duplicate <- reportedMap.get(id) - } yield duplicate - - optDuplicate.orElse { - Try { + val optDuplicate = sanitizedId.flatMap { id => + val reportedMap = reported.getAndUpdate(map => + if (map.contains(id)) map else map + (id -> path) + ) + reportedMap.get(id) + } + + ju.Optional.of( + optDuplicate.getOrElse { path.createDirectories() - path.writeText(sanitize(report.fullText(withIdAndSummary = true))) + path.writeText(sanitize(report.fullText(true))) path - }.toOption - } + } + ) } - override def sanitize(text: String): String = { - val textAfterWokspaceReplace = - text.replace(workspace.toString(), StdReportContext.WORKSPACE_STR) - userHome - .map(textAfterWokspaceReplace.replace(_, StdReportContext.HOME_STR)) - .getOrElse(textAfterWokspaceReplace) - } + override def sanitize(text: String): String = sanitizer(text) private def reportPath(report: Report): Path = { val date = TimeFormatter.getDate() val time = TimeFormatter.getTime() val buildTargetPart = - resolveBuildTarget(report.path).map("_(" ++ _ ++ ")").getOrElse("") + resolveBuildTarget(report.path.asScala) + .map("_(" ++ _ ++ ")") + .getOrElse("") val filename = s"r_${report.name}${buildTargetPart}_${time}.md" reportsDir.resolve(date).resolve(filename) } override def cleanUpOldReports( maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS - ): List[TimestampedFile] = limitedFilesManager.deleteOld(maxReportsNumber) + ): ju.List[TimestampedFile] = + limitedFilesManager.deleteOld(maxReportsNumber).asJava - override def getReports(): List[TimestampedFile] = - limitedFilesManager.getAllFiles() + override def getReports(): ju.List[TimestampedFile] = + limitedFilesManager.getAllFiles().asJava override def deleteAll(): Unit = { - getReports().foreach(r => Files.delete(r.toPath)) + getReports().forEach(r => Files.delete(r.toPath)) limitedFilesManager.directoriesWithDate.foreach { d => Files.delete(d.toPath) } @@ -190,16 +167,57 @@ object StdReportContext { def reportsDir: Path = Paths.get(".metals").resolve(".reports") } +/** + * Fan-out report context delegating reporting to all underlying reporters of given type. + */ +class MirroredReportContext(primary: ReportContext, auxilary: ReportContext*) + extends ReportContext { + private def mirror(selector: ReportContext => Reporter): Reporter = + new MirroredReporter(selector(primary), auxilary.map(selector): _*) + override lazy val unsanitized: Reporter = mirror(_.unsanitized) + override lazy val incognito: Reporter = mirror(_.incognito) + override lazy val bloop: Reporter = mirror(_.bloop) +} + +private class MirroredReporter( + primaryReporter: Reporter, + auxilaryReporters: Reporter* +) extends Reporter { + override val name: String = + s"${primaryReporter.name}-mirror-${auxilaryReporters.map(_.name).mkString("|")}" + private final def allReporters = primaryReporter :: auxilaryReporters.toList + + override def create(report: Report, ifVerbose: Boolean): ju.Optional[Path] = { + auxilaryReporters.foreach(_.create(report, ifVerbose)) + primaryReporter.create(report, ifVerbose) + } + + override def cleanUpOldReports( + maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS + ): ju.List[TimestampedFile] = + allReporters.flatMap(_.cleanUpOldReports(maxReportsNumber).asScala).asJava + + override def getReports(): ju.List[TimestampedFile] = + allReporters.flatMap(_.getReports().asScala).asJava + + override def deleteAll(): Unit = + allReporters.foreach(_.deleteAll()) +} + object EmptyReporter extends Reporter { + Boolean override def name = "empty-reporter" - override def create(report: => Report, ifVerbose: Boolean): Option[Path] = - None + override def create(report: Report, ifVerbose: Boolean): ju.Optional[Path] = + ju.Optional.empty() - override def cleanUpOldReports(maxReportsNumber: Int): List[TimestampedFile] = - List() + override def cleanUpOldReports( + maxReportsNumber: Int = StdReportContext.MAX_NUMBER_OF_REPORTS + ): ju.List[TimestampedFile] = + ju.Collections.emptyList() - override def getReports(): List[TimestampedFile] = List() + override def getReports(): ju.List[TimestampedFile] = + ju.Collections.emptyList() override def deleteAll(): Unit = {} } @@ -213,73 +231,6 @@ object EmptyReportContext extends ReportContext { override def bloop: Reporter = EmptyReporter } -case class Report( - name: String, - text: String, - shortSummary: String, - path: Option[String] = None, - id: Option[String] = None, - error: Option[Throwable] = None -) { - def extend(moreInfo: String): Report = - this.copy( - text = s"""|${this.text} - |$moreInfo"""".stripMargin - ) - - def fullText(withIdAndSummary: Boolean): String = { - val sb = new StringBuilder - if (withIdAndSummary) { - id.foreach(id => sb.append(s"${Report.idPrefix}$id\n")) - } - path.foreach(path => sb.append(s"$path\n")) - error match { - case Some(error) => - sb.append( - s"""|### $error - | - |$text - | - |#### Error stacktrace: - | - |``` - |${error.getStackTrace().mkString("\n\t")} - |``` - |""".stripMargin - ) - case None => sb.append(s"$text\n") - } - if (withIdAndSummary) - sb.append(s"""|${Report.summaryTitle} - | - |$shortSummary""".stripMargin) - sb.result() - } -} - -object Report { - - def apply( - name: String, - text: String, - error: Throwable, - path: Option[String] - ): Report = - Report( - name, - text, - shortSummary = error.toString(), - path = path, - error = Some(error) - ) - - def apply(name: String, text: String, error: Throwable): Report = - Report(name, text, error, path = None) - - val idPrefix = "error id: " - val summaryTitle = "#### Short summary: " -} - sealed trait ReportLevel { def isVerbose: Boolean } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportSanitizer.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportSanitizer.scala new file mode 100644 index 00000000000..5149b7fcb21 --- /dev/null +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/ReportSanitizer.scala @@ -0,0 +1,40 @@ +package scala.meta.internal.metals + +import java.nio.file.Path + +import scala.meta.internal.mtags.CommonMtagsEnrichments.XtensionOptionalJava +import scala.meta.internal.pc.StandardReport +import scala.meta.pc.Report + +trait ReportSanitizer { + final def apply(report: Report): Report = { + val sanitizedPath: Option[String] = report.path.asScala.map(sanitize(_)) + val sanitizedId: Option[String] = report.id.asScala.map(sanitize(_)) + StandardReport( + name = sanitize(report.name), + text = sanitize(report.text), + shortSummary = sanitize(report.shortSummary), + path = sanitizedPath, + id = sanitizedId + ) + } + + final def apply(text: String): String = sanitize(text) + + def sanitize(text: String): String +} + +class WorkspaceSanitizer(workspace: Option[Path]) extends ReportSanitizer { + private lazy val userHome = Option(System.getProperty("user.home")) + + override def sanitize(text: String): String = { + val textAfterWokspaceReplace = workspace + .map(_.toString()) + .foldLeft(text)( + _.replace(_, StdReportContext.WORKSPACE_STR) + ) + userHome.foldLeft(textAfterWokspaceReplace)( + _.replace(_, StdReportContext.HOME_STR) + ) + } +} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryConfiguration.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryConfiguration.scala new file mode 100644 index 00000000000..5b51e676ee9 --- /dev/null +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryConfiguration.scala @@ -0,0 +1,71 @@ +package scala.meta.internal.metals + +/** + * Telemetry configuration class. + * + * @param telemetryLevel what kinds of data should we send to the telemetry server + * @param includeCodeSnippet should we also include code snippets that caused the crash + */ +case class TelemetryConfiguration( + telemetryLevel: TelemetryLevel, + includeCodeSnippet: Boolean +) + +object TelemetryConfiguration { + def default: TelemetryConfiguration = + TelemetryConfiguration(TelemetryLevel.default, false) +} + +/** + * Telemetry level set by the user or downgraded to the editor telemetry level. + * It controls the kind of reports being sent and its verbosity level. + */ +trait TelemetryLevel { + val textValue: String + val enabled: Boolean + protected val level: Int + + def sendCrashes: Boolean = level >= 1 + def sendErrors: Boolean = level >= 2 + def sendUsage: Boolean = level >= 3 +} + +object TelemetryLevel { + + implicit val levelOrdering: Ordering[TelemetryLevel] = Ordering.by(_.level) + + case object Off extends TelemetryLevel { + val textValue: String = "off" + val level: Int = 0 + val enabled = false + } + + case object Crash extends TelemetryLevel { + val textValue: String = "crash" + val level: Int = 1 + val enabled = true + } + + case object Error extends TelemetryLevel { + val textValue: String = "error" + val level: Int = 2 + val enabled = true + } + + case object All extends TelemetryLevel { + val textValue: String = "all" + val level: Int = 3 + val enabled = true + } + + val default = Off + + def fromString(textValue: String): TelemetryLevel = { + textValue match { + case Crash.textValue => Crash + case Error.textValue => Error + case All.textValue => All + case _ => Off + } + } +} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala index e2bff5dc7cc..1e9531daf42 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/utils/LimitedFilesManager.scala @@ -8,6 +8,7 @@ import scala.util.Try import scala.util.matching.Regex import scala.meta.internal.metals.TimeFormatter +import scala.meta.pc.TimestampedFile class LimitedFilesManager( directory: Path, @@ -53,7 +54,7 @@ class LimitedFilesManager( reMatch <- prefixRegex.findPrefixMatchOf(file.getName()) timeStr = reMatch.after.toString.stripSuffix(extension) time <- Try(timeStr.toLong).toOption - } yield TimestampedFile(file, time) + } yield new TimestampedFile(file, time) } private def filesWithDate(dir: File): List[TimestampedFile] = { @@ -69,7 +70,7 @@ class LimitedFilesManager( case WithTimestamp(time) => TimeFormatter .parse(time, date) - .map(timestamp => TimestampedFile(file, timestamp)) + .map(timestamp => new TimestampedFile(file, timestamp)) case _ => None } } @@ -83,8 +84,3 @@ class LimitedFilesManager( } } } - -case class TimestampedFile(file: File, timestamp: Long) { - def toPath: Path = file.toPath() - def name: String = file.getName() -} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala b/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala index 8d73f9a9b0f..ac745183b8e 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerAccess.scala @@ -10,11 +10,10 @@ import java.util.logging.Logger import scala.concurrent.ExecutionContextExecutor import scala.util.control.NonFatal -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.CommonMtagsEnrichments._ import scala.meta.pc.CancelToken import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import scala.meta.pc.VirtualFileParams /** @@ -205,7 +204,7 @@ abstract class CompilerAccess[Reporter, Compiler]( ): Unit = { val error = CompilerThrowable.trimStackTrace(e) val report = - Report( + StandardReport( "compiler-error", s"""|occurred in the presentation compiler. | @@ -219,7 +218,7 @@ abstract class CompilerAccess[Reporter, Compiler]( path = params.map(_.uri().toString) ) val pathToReport = - rc.unsanitized.create(report) + rc.unsanitized.create(report, /* ifVerbose */ false).asScala pathToReport match { case Some(path) => logger.log( diff --git a/mtags-shared/src/main/scala/scala/meta/internal/pc/StandardReport.scala b/mtags-shared/src/main/scala/scala/meta/internal/pc/StandardReport.scala new file mode 100644 index 00000000000..50aaa3d40d4 --- /dev/null +++ b/mtags-shared/src/main/scala/scala/meta/internal/pc/StandardReport.scala @@ -0,0 +1,90 @@ +package scala.meta.internal.pc + +import java.{util => ju} + +import scala.meta.internal.mtags.CommonMtagsEnrichments._ +import scala.meta.pc.Report + +case class StandardReport( + name: String, + text: String, + shortSummary: String, + path: ju.Optional[String], + id: ju.Optional[String], + error: ju.Optional[Throwable] +) extends Report { + + def extend(moreInfo: String): StandardReport = + this.copy( + text = s"""|${this.text} + |$moreInfo"""".stripMargin + ) + + def fullText(withIdAndSummary: Boolean): String = { + val sb = new StringBuilder + if (withIdAndSummary) { + id.asScala.foreach(id => sb.append(s"${StandardReport.idPrefix}$id\n")) + } + path.asScala.foreach(path => sb.append(s"$path\n")) + error.asScala match { + case Some(error) => + sb.append( + s"""|### $error + | + |$text + | + |#### Error stacktrace: + | + |``` + |${error.getStackTrace().mkString("\n\t")} + |``` + |""".stripMargin + ) + case None => sb.append(s"$text\n") + } + if (withIdAndSummary) + sb.append(s"""|${StandardReport.summaryTitle} + | + |$shortSummary""".stripMargin) + sb.result() + } +} + +object StandardReport { + def apply( + name: String, + text: String, + shortSummary: String, + path: Option[String] = None, + id: Option[String] = None, + error: Option[Throwable] = None + ): StandardReport = + StandardReport( + name, + text, + shortSummary, + path.asJava, + id.asJava, + error.asJava + ) + + def apply( + name: String, + text: String, + error: Throwable, + path: Option[String] + ): StandardReport = + StandardReport( + name, + text, + shortSummary = error.toString(), + path = path, + error = Some(error) + ) + + def apply(name: String, text: String, error: Throwable): StandardReport = + StandardReport(name, text, error, path = None) + + val idPrefix = "error id: " + val summaryTitle = "#### Short summary: " +} diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala index fd981011d21..50cdc8c6b9a 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/HoverProvider.scala @@ -3,12 +3,12 @@ package scala.meta.internal.pc import scala.reflect.internal.util.Position import scala.reflect.internal.{Flags => gf} -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.pc.HoverSignature import scala.meta.pc.OffsetParams import scala.meta.pc.RangeParams +import scala.meta.pc.Report +import scala.meta.pc.ReportContext class HoverProvider(val compiler: MetalsGlobal, params: OffsetParams)(implicit reportContext: ReportContext @@ -53,7 +53,7 @@ class HoverProvider(val compiler: MetalsGlobal, params: OffsetParams)(implicit if (tree.pos.isDefined) tree.pos.start else pos.start - Report( + StandardReport( "empty-hover-scala2", s"""|pos: ${pos.toLsp} | @@ -295,7 +295,7 @@ class HoverProvider(val compiler: MetalsGlobal, params: OffsetParams)(implicit } if (result.isEmpty) { - report.foreach(reportContext.unsanitized.create(_, ifVerbose = true)) + report.foreach(reportContext.unsanitized.create(_, true)) } result } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala index 3d82a6783c1..2311a7f3cb9 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala @@ -7,8 +7,8 @@ import scala.tools.nsc.interactive.ShutdownReq import scala.tools.nsc.reporters.StoreReporter import scala.util.control.NonFatal -import scala.meta.internal.metals.ReportContext import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext class ScalaCompilerWrapper(global: MetalsGlobal) extends CompilerWrapper[StoreReporter, MetalsGlobal] { diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala index 7fad8e3fc10..76627dfa453 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -22,7 +22,7 @@ import scala.meta.internal.jdk.CollectionConverters._ import scala.meta.internal.metals.CompilerVirtualFileParams import scala.meta.internal.metals.EmptyCancelToken import scala.meta.internal.metals.EmptyReportContext -import scala.meta.internal.metals.ReportContext +import scala.meta.internal.metals.MirroredReportContext import scala.meta.internal.metals.ReportLevel import scala.meta.internal.metals.StdReportContext import scala.meta.internal.mtags.BuildInfo @@ -37,6 +37,7 @@ import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompiler import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.RangeParams +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import scala.meta.pc.VirtualFileParams import scala.meta.pc.{PcSymbolInformation => IPcSymbolInformation} @@ -61,7 +62,8 @@ case class ScalaPresentationCompiler( sh: Option[ScheduledExecutorService] = None, config: PresentationCompilerConfig = PresentationCompilerConfigImpl(), folderPath: Option[Path] = None, - reportsLevel: ReportLevel = ReportLevel.Info + reportsLevel: ReportLevel = ReportLevel.Info, + additionalReportContexts: List[ReportContext] = Nil ) extends PresentationCompiler { implicit val executionContext: ExecutionContextExecutor = ec @@ -71,11 +73,14 @@ case class ScalaPresentationCompiler( val logger: Logger = Logger.getLogger(classOf[ScalaPresentationCompiler].getName) - implicit val reportContex: ReportContext = - folderPath + implicit val implicitReportContex: ReportContext = { + val localReporters = folderPath .map(new StdReportContext(_, _ => buildTargetName, reportsLevel)) .getOrElse(EmptyReportContext) + new MirroredReportContext(localReporters, additionalReportContexts: _*) + } + override def withBuildTargetName( buildTargetName: String ): ScalaPresentationCompiler = @@ -84,6 +89,11 @@ case class ScalaPresentationCompiler( override def withReportsLoggerLevel(level: String): PresentationCompiler = copy(reportsLevel = ReportLevel.fromString(level)) + override def withAdditionalReportContexts( + additionalReportContexts: ju.List[ReportContext] + ): PresentationCompiler = + copy(additionalReportContexts = additionalReportContexts.asScala.toList) + override def withSearch(search: SymbolSearch): PresentationCompiler = copy(search = search) @@ -124,7 +134,7 @@ case class ScalaPresentationCompiler( } )( ec, - reportContex + implicitReportContex ) override def shutdown(): Unit = { diff --git a/mtags/src/main/scala-3-wrapper/ScalaPresentationCompiler.scala b/mtags/src/main/scala-3-wrapper/ScalaPresentationCompiler.scala index afd6dead724..55543dd1c9c 100644 --- a/mtags/src/main/scala-3-wrapper/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-3-wrapper/ScalaPresentationCompiler.scala @@ -22,6 +22,7 @@ import scala.meta.pc.PcSymbolInformation import scala.meta.pc.PresentationCompiler import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.RangeParams +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import scala.meta.pc.VirtualFileParams @@ -56,19 +57,22 @@ case class ScalaPresentationCompiler( config: PresentationCompilerConfig = PresentationCompilerConfigImpl(), folderPath: Option[Path] = None, reportsLevel: ReportLevel = ReportLevel.Info, + additionalReportContexts: List[ReportContext] = Nil, ) extends PresentationCompiler: - val underlying: DottyPresentationCompiler = new DottyPresentationCompiler( - buildTargetIdentifier = buildTargetIdentifier, - buildTargetName = buildTargetName, - classpath = classpath, - options = options, - search = search, - ec = ec, - sh = sh, - config = config, - folderPath = folderPath, - reportsLevel = reportsLevel, - ) + val underlying: PresentationCompiler = + val pc = new DottyPresentationCompiler( + buildTargetIdentifier = buildTargetIdentifier, + buildTargetName = buildTargetName, + classpath = classpath, + options = options, + search = search, + ec = ec, + sh = sh, + config = config, + folderPath = folderPath, + reportsLevel = reportsLevel, + ) + pc.withAdditionalReportContexts(additionalReportContexts.asJava) def this() = this("", None, Nil, Nil) @@ -243,6 +247,11 @@ case class ScalaPresentationCompiler( override def withReportsLoggerLevel(level: String): PresentationCompiler = copy(reportsLevel = ReportLevel.fromString(level)) + override def withAdditionalReportContexts( + additionalReportContexts: ju.List[ReportContext] + ): PresentationCompiler = + copy(additionalReportContexts = additionalReportContexts.asScala.toList) + override def inlineValue( params: OffsetParams ): CompletableFuture[ju.List[TextEdit]] = diff --git a/mtags/src/main/scala-3/scala/meta/internal/mtags/MtagsEnrichments.scala b/mtags/src/main/scala-3/scala/meta/internal/mtags/MtagsEnrichments.scala index f2a124acd48..193b6bedcaf 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/mtags/MtagsEnrichments.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/mtags/MtagsEnrichments.scala @@ -53,7 +53,7 @@ object MtagsEnrichments extends ScalametaCommonEnrichments: new SourcePosition(source, span) end sourcePosition - def latestRun = + def latestRun = if driver.currentCtx.run.units.nonEmpty then driver.currentCtx.run.units.head else diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/AutoImportsProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/AutoImportsProvider.scala index cf27a1275cd..6b39ca1c7d9 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/AutoImportsProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/AutoImportsProvider.scala @@ -5,11 +5,11 @@ import java.nio.file.Paths import scala.collection.mutable import scala.jdk.CollectionConverters.* -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.AutoImports.* import scala.meta.internal.pc.completions.CompletionPos import scala.meta.pc.* +import scala.meta.pc.ReportContext import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Symbols.* diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala index feb6c62931e..1c951063a0c 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala @@ -5,9 +5,8 @@ import java.util.logging.Logger import scala.util.control.NonFatal -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.pc.* +import scala.meta.pc.ReportContext import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Flags @@ -36,7 +35,7 @@ class CompilerSearchVisitor( false case NonFatal(error) => reports.incognito.create( - Report( + StandardReport( "is_public", s"""Symbol: $sym""".stripMargin, error, diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/ExtractMethodProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/ExtractMethodProvider.scala index 3aa9d6edf06..84b9fde9590 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/ExtractMethodProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/ExtractMethodProvider.scala @@ -4,12 +4,12 @@ import java.nio.file.Paths import scala.meta as m -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.internal.pc.printer.MetalsPrinter.IncludeDefaultParam import scala.meta.pc.OffsetParams import scala.meta.pc.RangeParams +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.Trees.* diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala index 2b9e1a2de57..3c6ed6493fd 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/HoverProvider.scala @@ -2,12 +2,11 @@ package scala.meta.internal.pc import java.util as ju -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.pc.HoverSignature import scala.meta.pc.OffsetParams +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd.* @@ -57,7 +56,7 @@ object HoverProvider: if path.isEmpty || path.head.sourcePos == null || !path.head.sourcePos.exists then pos.start else path.head.sourcePos.start - Report( + StandardReport( "empty-hover-scala3", s"""|pos: ${pos.toLsp} | @@ -89,7 +88,7 @@ object HoverProvider: path = Some(uri.toString), ) end report - reportContext.unsanitized.create(report, ifVerbose = true) + reportContext.unsanitized.create(report) ju.Optional.empty() else val skipCheckOnName = diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/InferredTypeProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/InferredTypeProvider.scala index 12757cc903b..def3b908aff 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/InferredTypeProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/InferredTypeProvider.scala @@ -5,12 +5,12 @@ import java.nio.file.Paths import scala.annotation.tailrec import scala.meta as m -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.internal.pc.printer.ShortenedNames import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.Trees.* diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/PcInlayHintsProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/PcInlayHintsProvider.scala index 4d398274c67..68fcc53e5a7 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/PcInlayHintsProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/PcInlayHintsProvider.scala @@ -2,11 +2,11 @@ package scala.meta.internal.pc import java.nio.file.Paths -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.internal.pc.printer.ShortenedNames import scala.meta.pc.InlayHintsParams +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/Scala3CompilerAccess.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/Scala3CompilerAccess.scala index eb8a6ae24af..e604184adcd 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/Scala3CompilerAccess.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/Scala3CompilerAccess.scala @@ -4,8 +4,8 @@ import java.util.concurrent.ScheduledExecutorService import scala.concurrent.ExecutionContextExecutor -import scala.meta.internal.metals.ReportContext import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import dotty.tools.dotc.reporting.StoreReporter diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala index 66ffd1a321e..8171624d5e7 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -8,6 +8,7 @@ import java.util.Optional import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutorService import java.util.concurrent.ScheduledExecutorService +import java.util.logging.Logger import scala.collection.JavaConverters.* import scala.concurrent.ExecutionContext @@ -16,7 +17,7 @@ import scala.concurrent.ExecutionContextExecutor import scala.meta.internal.metals.CompilerVirtualFileParams import scala.meta.internal.metals.EmptyCancelToken import scala.meta.internal.metals.EmptyReportContext -import scala.meta.internal.metals.ReportContext +import scala.meta.internal.metals.MirroredReportContext import scala.meta.internal.metals.ReportLevel import scala.meta.internal.metals.StdReportContext import scala.meta.internal.mtags.BuildInfo @@ -25,6 +26,7 @@ import scala.meta.internal.pc.completions.CompletionProvider import scala.meta.internal.pc.completions.OverrideCompletions import scala.meta.pc.* import scala.meta.pc.PcSymbolInformation as IPcSymbolInformation +import scala.meta.pc.ReportContext import dotty.tools.dotc.reporting.StoreReporter import org.eclipse.lsp4j as l @@ -42,6 +44,7 @@ case class ScalaPresentationCompiler( config: PresentationCompilerConfig = PresentationCompilerConfigImpl(), folderPath: Option[Path] = None, reportsLevel: ReportLevel = ReportLevel.Info, + additionalReportContexts: List[ReportContext] = Nil, ) extends PresentationCompiler: def this() = this("", None, Nil, Nil) @@ -50,17 +53,28 @@ case class ScalaPresentationCompiler( private val forbiddenOptions = Set("-print-lines", "-print-tasty") private val forbiddenDoubleOptions = Set("-release") + + val logger: Logger = + Logger.getLogger(classOf[ScalaPresentationCompiler].getName) + given ReportContext = - folderPath - .map(StdReportContext(_, _ => buildTargetName, reportsLevel)) + val localReporters = folderPath + .map(new StdReportContext(_, _ => buildTargetName, reportsLevel)) .getOrElse(EmptyReportContext) + new MirroredReportContext(localReporters, additionalReportContexts*) + override def withBuildTargetName(buildTargetName: String) = copy(buildTargetName = Some(buildTargetName)) override def withReportsLoggerLevel(level: String): PresentationCompiler = copy(reportsLevel = ReportLevel.fromString(level)) + override def withAdditionalReportContexts( + additionalReportContexts: ju.List[ReportContext] + ): PresentationCompiler = + copy(additionalReportContexts = additionalReportContexts.asScala.toList) + val compilerAccess: CompilerAccess[StoreReporter, MetalsDriver] = Scala3CompilerAccess( config, diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala index 6da5f175824..1438bfbc68b 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala @@ -5,13 +5,13 @@ import java.nio.file.Path import scala.collection.JavaConverters.* -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.AutoImports.AutoImportEdits import scala.meta.internal.pc.AutoImports.AutoImportsGenerator import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala index 42ab60bd554..d1f7cb91dcd 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala @@ -7,7 +7,6 @@ import java.nio.file.Paths import scala.collection.mutable import scala.meta.internal.metals.Fuzzy -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.BuildInfo import scala.meta.internal.mtags.CoursierComplete import scala.meta.internal.mtags.MtagsEnrichments.* @@ -15,6 +14,7 @@ import scala.meta.internal.pc.AutoImports.AutoImportsGenerator import scala.meta.internal.pc.completions.OverrideCompletions.OverrideExtractor import scala.meta.internal.semver.SemVer import scala.meta.pc.* +import scala.meta.pc.ReportContext import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Constants.Constant diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/InterpolatorCompletions.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/InterpolatorCompletions.scala index e2859fc444c..eaef77c10d3 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/InterpolatorCompletions.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/InterpolatorCompletions.scala @@ -2,13 +2,13 @@ package scala.meta.internal.pc.completions import scala.collection.mutable.ListBuffer -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.CompilerSearchVisitor import scala.meta.internal.pc.CompletionFuzzy import scala.meta.internal.pc.IndexedContext import scala.meta.internal.pc.InterpolationSplice import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd.* diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/MatchCaseCompletions.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/MatchCaseCompletions.scala index 208f0892b05..d86a0e28e07 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/MatchCaseCompletions.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/MatchCaseCompletions.scala @@ -7,7 +7,6 @@ import scala.collection.JavaConverters.* import scala.collection.mutable import scala.collection.mutable.ListBuffer -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.AutoImports.AutoImportsGenerator import scala.meta.internal.pc.AutoImports.SymbolImport @@ -15,6 +14,7 @@ import scala.meta.internal.pc.MetalsInteractive.* import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.internal.pc.printer.MetalsPrinter.IncludeDefaultParam import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd.* diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/OverrideCompletions.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/OverrideCompletions.scala index 5f7e661ace8..84363974966 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/OverrideCompletions.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/OverrideCompletions.scala @@ -5,7 +5,6 @@ import java.util as ju import scala.collection.JavaConverters.* -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.AutoImports.AutoImport import scala.meta.internal.pc.AutoImports.AutoImportsGenerator @@ -13,6 +12,7 @@ import scala.meta.internal.pc.printer.MetalsPrinter import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.PresentationCompilerConfig.OverrideDefFormat +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd.* diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala index 633899cba5f..c2767b99336 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/printer/MetalsPrinter.scala @@ -6,13 +6,13 @@ import scala.util.Success import scala.util.Try import scala.meta.internal.jdk.CollectionConverters.* -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.MtagsEnrichments.* import scala.meta.internal.pc.Compat.EvidenceParamName import scala.meta.internal.pc.IndexedContext import scala.meta.internal.pc.Params +import scala.meta.internal.pc.StandardReport import scala.meta.internal.pc.printer.ShortenedNames.ShortName +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch @@ -76,16 +76,17 @@ class MetalsPrinter( case Success(short) => short case Failure(e) => val reportContext = summon[ReportContext] - val report = Report( + val report = StandardReport( "short-name-error", - s"""|Error while printing type, could not create short name for type: + s"""|Error while printing type, could not create short name for type: |$tpe |""".stripMargin, e.toString, - id = Some(tpe.typeSymbol.name.show), - error = Some(e), + java.util.Optional.empty(), + Some(tpe.typeSymbol.name.show).asJava, + Some(e).asJava, ) - reportContext.unsanitized.create(report, ifVerbose = false) + reportContext.unsanitized.create(report, /* ifVerbose */ false) tpe dotcPrinter.tpe(short) end tpe diff --git a/mtags/src/main/scala/scala/meta/internal/metals/Docstrings.scala b/mtags/src/main/scala/scala/meta/internal/metals/Docstrings.scala index 0efcb4a4154..6083d980151 100644 --- a/mtags/src/main/scala/scala/meta/internal/metals/Docstrings.scala +++ b/mtags/src/main/scala/scala/meta/internal/metals/Docstrings.scala @@ -21,6 +21,7 @@ import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.SymbolOccurrence import scala.meta.io.AbsolutePath import scala.meta.pc.ParentSymbols +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolDocumentation /** diff --git a/mtags/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala b/mtags/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala index 21029813fb6..4ff47ff49ae 100644 --- a/mtags/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala +++ b/mtags/src/main/scala/scala/meta/internal/metals/SemanticdbDefinition.scala @@ -11,6 +11,7 @@ import scala.meta.internal.semanticdb.Language import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.SymbolOccurrence import scala.meta.internal.{semanticdb => s} +import scala.meta.pc.ReportContext import scala.meta.tokenizers.TokenizeException import org.eclipse.{lsp4j => l} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala index e52b9fcfa11..1e90d07cf58 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala @@ -4,12 +4,12 @@ import scala.meta.Dialect import scala.meta.dialects import scala.meta.inputs.Input import scala.meta.internal.metals.EmptyReportContext -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.ScalametaCommonEnrichments._ import scala.meta.internal.semanticdb.Language import scala.meta.internal.semanticdb.Scala._ import scala.meta.internal.semanticdb.TextDocument import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext final class Mtags(implicit rc: ReportContext) { def totalLinesOfCode: Long = javaLines + scalaLines diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala index a7b47aa3ba3..d1088de499a 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala @@ -9,8 +9,8 @@ import scala.util.control.NonFatal import scala.meta.Dialect import scala.meta.dialects import scala.meta.internal.io.{ListFiles => _} -import scala.meta.internal.metals.ReportContext import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext /** * An implementation of GlobalSymbolIndex with fast indexing and low memory usage. diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala index 55df0ab642f..0309ac75a25 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala @@ -6,9 +6,8 @@ import scala.meta.Dialect import scala.meta.inputs.Input import scala.meta.inputs.Position import scala.meta.internal.inputs._ -import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext import scala.meta.internal.mtags.ScalametaCommonEnrichments._ +import scala.meta.internal.pc.StandardReport import scala.meta.internal.semanticdb.Language import scala.meta.internal.semanticdb.Scala import scala.meta.internal.semanticdb.Scala._ @@ -16,6 +15,7 @@ import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.internal.semanticdb.SymbolInformation.Kind import scala.meta.internal.tokenizers.LegacyScanner import scala.meta.internal.tokenizers.LegacyToken._ +import scala.meta.pc.ReportContext import scala.meta.tokenizers.TokenizeException final class Identifier(val name: String, val pos: Position) { @@ -1039,7 +1039,7 @@ class ScalaToplevelMtags( def reportError(expected: String): Unit = { rc.incognito.create( - Report( + StandardReport( "scala-toplevel-mtags", failMessage(expected), s"expected $expected; obtained $currentToken", diff --git a/project/TestGroups.scala b/project/TestGroups.scala index a2c0dfba332..e83b996b4cc 100644 --- a/project/TestGroups.scala +++ b/project/TestGroups.scala @@ -116,7 +116,11 @@ object TestGroups { "tests.decorations.SyntheticDecorationsExpectSuite", "tests.codeactions.ConvertSingleLineCommentLspSuite", "tests.ServerLivenessMonitorSuite", "tests.ResetWorkspaceLspSuite", - "tests.ToplevelWithInnerScala3Suite"), + "tests.ToplevelWithInnerScala3Suite", + "tests.telemetry.RemoteReporterSuite", + "tests.telemetry.TelemetryReporterSuite", + "tests.telemetry.SerializationSuite", + "tests.telemetry.SourceCodeSanitizerSuite"), ) } diff --git a/project/V.scala b/project/V.scala index de304400287..cb072ae2cd0 100644 --- a/project/V.scala +++ b/project/V.scala @@ -31,6 +31,7 @@ object V { val genyVersion = "1.0.0" val gitter8Version = "0.16.2" val gradleBloop = "1.6.2" + val gson = "2.10.1" val java8Compat = "1.0.2" val javaSemanticdb = "0.9.9" val jsoup = "1.17.2" @@ -41,6 +42,7 @@ object V { val mdoc = "2.5.2" val munit = "1.0.0-M11" val pprint = "0.7.3" + val requests = "0.8.0" val sbtBloop = bloop val sbtJdiTools = "1.1.1" val scalaCli = "1.2.0" diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Environment.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Environment.scala new file mode 100644 index 00000000000..a80ecaa0978 --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Environment.scala @@ -0,0 +1,19 @@ +package scala.meta.internal.telemetry + +case class Environment(java: JavaInfo, system: SystemInfo) +object Environment { + val java: JavaInfo = JavaInfo( + System.getProperty("java.version", "unknown"), + System.getProperty("java.vendor", "unknown"), + ) + val system: SystemInfo = SystemInfo( + System.getProperty("os.arch", "unknown"), + System.getProperty("os.name", "unknown"), + System.getProperty("os.version", "unknown"), + ) + + val instance: Environment = Environment(java, system) +} + +case class JavaInfo(version: String, distribution: String) +case class SystemInfo(architecture: String, name: String, version: String) diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Exception.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Exception.scala new file mode 100644 index 00000000000..3afa1006d41 --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Exception.scala @@ -0,0 +1,37 @@ +package scala.meta.internal.telemetry + +case class ExceptionSummary( + exception: String, + stacktrace: List[StackTraceElement], +) + +case class StackTraceElement( + fullyQualifiedName: String, + methodName: String, + errorFile: String, + errorLine: Int, +) + +object ExceptionSummary { + def from(e: Throwable, sanitize: String => String): ExceptionSummary = { + val stackTraceElements = e + .getStackTrace() + .toList + .flatMap(element => { + for { + className <- Option(element.getClassName) + methodName <- Option(element.getMethodName) + errorLine <- Option(element.getLineNumber) + errorFile <- Option(element.getFileName) + } yield StackTraceElement( + sanitize(className), + sanitize(methodName), + sanitize(errorFile), + errorLine, + ) + }) + + ExceptionSummary(sanitize(e.getMessage), stackTraceElements) + } + +} diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/MetalsConfiguration.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/MetalsConfiguration.scala new file mode 100644 index 00000000000..5b0034fcbe9 --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/MetalsConfiguration.scala @@ -0,0 +1,29 @@ +package scala.meta.internal.telemetry + +case class MetalsUserConfiguration( + symbolPrefixes: Map[String, String], + bloopSbtAlreadyInstalled: Boolean, + bloopVersion: Option[String], + bloopJvmProperties: List[String], + ammoniteJvmProperties: List[String], + superMethodLensesEnabled: Boolean, + showInferredType: Option[String], + showImplicitArguments: Boolean, + showImplicitConversionsAndClasses: Boolean, + enableStripMarginOnTypeFormatting: Boolean, + enableIndentOnPaste: Boolean, + enableSemanticHighlighting: Boolean, + excludedPackages: List[String], + fallbackScalaVersion: Option[String], + testUserInterface: String, +) + +case class MetalsServerConfiguration( + executeClientCommand: String, + snippetAutoIndent: Boolean, + isHttpEnabled: Boolean, + isInputBoxEnabled: Boolean, + askToReconnect: Boolean, + allowMultilineStringFormatting: Boolean, + compilers: PresentationCompilerConfig, +) diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/PresentationCompilerConfig.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/PresentationCompilerConfig.scala new file mode 100644 index 00000000000..671358d13d8 --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/PresentationCompilerConfig.scala @@ -0,0 +1,17 @@ +package scala.meta.internal.telemetry + +case class PresentationCompilerConfig( + symbolPrefixes: Map[String, String], + completionCommand: Option[String], + parameterHintsCommand: Option[String], + overrideDefFormat: String, + isDefaultSymbolPrefixes: Boolean, + isCompletionItemDetailEnabled: Boolean, + isStripMarginOnTypeFormattingEnabled: Boolean, + isCompletionItemDocumentationEnabled: Boolean, + isHoverDocumentationEnabled: Boolean, + snippetAutoIndent: Boolean, + isSignatureHelpDocumentationEnabled: Boolean, + isCompletionSnippetsEnabled: Boolean, + semanticdbCompilerOptions: List[String], +) diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/ReporterContext.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/ReporterContext.scala new file mode 100644 index 00000000000..1ba8d388fca --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/ReporterContext.scala @@ -0,0 +1,19 @@ +package scala.meta.internal.telemetry + +sealed trait ReporterContext +case class ScalaPresentationCompilerContext( + scalaVersion: String, + options: List[String], + config: PresentationCompilerConfig, +) extends ReporterContext + +case class MetalsLspContext( + metalsVersion: String, + userConfig: MetalsUserConfiguration, + serverConfig: MetalsServerConfiguration, + clientInfo: MetalsClientInfo, + buildServerConnections: List[BuildServerConnection], +) extends ReporterContext + +case class MetalsClientInfo(name: Option[String], version: Option[String]) +case class BuildServerConnection(name: String, version: String, isMain: Boolean) diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Reports.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Reports.scala new file mode 100644 index 00000000000..f1e8e81a078 --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/Reports.scala @@ -0,0 +1,30 @@ +package scala.meta.internal.telemetry + +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.macros._ + +case class CrashReport( + error: ExceptionSummary, + componentName: String, + env: Environment = Environment.instance, + componentVersion: Option[String] = None, + reporterContext: Option[ReporterContext] = None, +) + +object CrashReport { + implicit val codec: JsonValueCodec[CrashReport] = JsonCodecMaker.make +} + +case class ErrorReport( + name: String, + reporterName: String, + reporterContext: ReporterContext, + env: Environment = Environment.instance, + text: String, + id: Option[String] = None, + error: Option[ExceptionSummary] = None, +) + +object ErrorReport { + implicit val codec: JsonValueCodec[ErrorReport] = JsonCodecMaker.make +} diff --git a/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/TelemetryEndpoints.scala b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/TelemetryEndpoints.scala new file mode 100644 index 00000000000..0b200699b9d --- /dev/null +++ b/telemetry-interfaces/src/main/scala/scala/meta/internal/telemetry/TelemetryEndpoints.scala @@ -0,0 +1,20 @@ +package scala.meta.internal.telemetry + +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.json.jsoniter._ + +object TelemetryEndpoints { + val baseEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = endpoint.in("v1.0") + + val sendErrorReport: PublicEndpoint[ErrorReport, Unit, Unit, Any] = + baseEndpoint.post + .in("reporting" / "sendErrorReport") + .in(jsonBody[ErrorReport]) + + val sendCrashReport: PublicEndpoint[CrashReport, Unit, Unit, Any] = + baseEndpoint.post + .in("reporting" / "sendCrashReport") + .in(jsonBody[CrashReport]) + +} diff --git a/tests/mtest/src/main/scala/tests/PCSuite.scala b/tests/mtest/src/main/scala/tests/PCSuite.scala index d74e0ec1d9e..97950bbc554 100644 --- a/tests/mtest/src/main/scala/tests/PCSuite.scala +++ b/tests/mtest/src/main/scala/tests/PCSuite.scala @@ -14,8 +14,8 @@ import scala.meta.internal.metals.Docstrings import scala.meta.internal.metals.EmptyReportContext import scala.meta.internal.metals.ExcludedPackagesHandler import scala.meta.internal.metals.JdkSources -import scala.meta.internal.metals.ReportContext import scala.meta.io.AbsolutePath +import scala.meta.pc.ReportContext import coursierapi.Fetch import coursierapi.Repository diff --git a/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala b/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala index 18ed873818d..179ccc914cd 100644 --- a/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala +++ b/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala @@ -7,7 +7,6 @@ import java.{util => ju} import scala.meta.internal.metals.ClasspathSearch import scala.meta.internal.metals.Docstrings import scala.meta.internal.metals.EmptyReportContext -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.WorkspaceSymbolInformation import scala.meta.internal.metals.WorkspaceSymbolQuery import scala.meta.internal.mtags.GlobalSymbolIndex @@ -16,6 +15,7 @@ import scala.meta.internal.mtags.OnDemandSymbolIndex import scala.meta.internal.mtags.Symbol import scala.meta.internal.{semanticdb => s} import scala.meta.pc.ParentSymbols +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import scala.meta.pc.SymbolSearchVisitor diff --git a/tests/mtest/src/main/scala/tests/TestingWorkspaceSearch.scala b/tests/mtest/src/main/scala/tests/TestingWorkspaceSearch.scala index 06647fbcb6b..9ac2529d426 100644 --- a/tests/mtest/src/main/scala/tests/TestingWorkspaceSearch.scala +++ b/tests/mtest/src/main/scala/tests/TestingWorkspaceSearch.scala @@ -7,11 +7,11 @@ import scala.collection.mutable import scala.meta.Dialect import scala.meta.inputs.Input import scala.meta.internal.metals.EmptyReportContext -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.SemanticdbDefinition import scala.meta.internal.metals.WorkspaceSymbolInformation import scala.meta.internal.metals.WorkspaceSymbolQuery import scala.meta.internal.mtags.ScalametaCommonEnrichments.XtensionWorkspaceSymbolQuery +import scala.meta.pc.ReportContext import scala.meta.pc.SymbolSearchVisitor object TestingWorkspaceSearch { diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index 29fadfd13f3..c4891d31c12 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -50,7 +50,6 @@ import scala.meta.internal.metals.MtagsResolver import scala.meta.internal.metals.ParametrizedCommand import scala.meta.internal.metals.PositionSyntax._ import scala.meta.internal.metals.ProgressTicks -import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.ScalaVersionSelector import scala.meta.internal.metals.ServerCommands import scala.meta.internal.metals.StdReportContext @@ -73,6 +72,7 @@ import scala.meta.internal.tvp.TreeViewVisibilityDidChangeParams import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.meta.io.RelativePath +import scala.meta.pc.ReportContext import ch.epfl.scala.{bsp4j => b} import com.google.gson.JsonElement diff --git a/tests/unit/src/test/scala/tests/BspStatusSuite.scala b/tests/unit/src/test/scala/tests/BspStatusSuite.scala index 7296bbdcf0f..5a796c74744 100644 --- a/tests/unit/src/test/scala/tests/BspStatusSuite.scala +++ b/tests/unit/src/test/scala/tests/BspStatusSuite.scala @@ -101,10 +101,10 @@ class BspStatusSuite extends BaseLspSuite("bsp-status-suite") { s"Bloop 1 ${Icons.default.alert}", ) reports = bloopReports - _ = assert(reports.nonEmpty) - reportUri = reports.head.file.toURI().toString + _ = assert(!reports.isEmpty) + reportUri = reports.get(0).file.toURI().toString newPath = (reportUri ++ ".seen").toAbsolutePath.toNIO - _ = Files.move(reports.head.toPath, newPath) + _ = Files.move(reports.get(0).toPath, newPath) _ <- server.fullServer .didChangeWatchedFiles( new DidChangeWatchedFilesParams( @@ -113,7 +113,7 @@ class BspStatusSuite extends BaseLspSuite("bsp-status-suite") { ) .asScala _ = assertNoDiff(client.pollStatusBar(), s"Bloop ${Icons.default.link}") - _ = assert(bloopReports.nonEmpty) + _ = assert(!bloopReports.isEmpty) } yield () } diff --git a/tests/unit/src/test/scala/tests/ReportsSuite.scala b/tests/unit/src/test/scala/tests/ReportsSuite.scala index 0a600ed8066..150180138c6 100644 --- a/tests/unit/src/test/scala/tests/ReportsSuite.scala +++ b/tests/unit/src/test/scala/tests/ReportsSuite.scala @@ -4,15 +4,17 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths +import scala.meta.internal.jdk.CollectionConverters._ import scala.meta.internal.metals.FolderReportsZippper import scala.meta.internal.metals.Icons -import scala.meta.internal.metals.Report import scala.meta.internal.metals.ReportFileName import scala.meta.internal.metals.StdReportContext import scala.meta.internal.metals.TimeFormatter import scala.meta.internal.metals.ZipReportsProvider import scala.meta.internal.metals.doctor.Doctor +import scala.meta.internal.pc.StandardReport import scala.meta.io.AbsolutePath +import scala.meta.pc.Report class ReportsSuite extends BaseSuite { val workspace: AbsolutePath = AbsolutePath(Paths.get(".")) @@ -33,7 +35,7 @@ class ReportsSuite extends BaseSuite { |""".stripMargin def exampleReport(name: String, path: Option[String] = None): Report = - Report(name, exampleText(), "Test error report.", path) + StandardReport(name, exampleText(), "Test error report.", path) override def afterEach(context: AfterEach): Unit = { reportsProvider.deleteAll() @@ -47,13 +49,13 @@ class ReportsSuite extends BaseSuite { new String(Files.readAllBytes(path.get), StandardCharsets.UTF_8) assertNoDiff( s"""|${exampleText(StdReportContext.WORKSPACE_STR)} - |#### Short summary: + |#### Short summary: | |Test error report. |""".stripMargin, obtained, ) - assert(reportsProvider.incognito.getReports().length == 1) + assert(reportsProvider.incognito.getReports().size() == 1) val dirsWithDate = reportsProvider.reportsDir.resolve("metals").toFile().listFiles() assert(dirsWithDate.length == 1) @@ -71,6 +73,7 @@ class ReportsSuite extends BaseSuite { reportsProvider.incognito.create(report2) val reports = reportsProvider.incognito .getReports() + .asScala .map { report => val (name, buildTarget) = ReportFileName.getReportNameAndBuildTarget(report) @@ -101,12 +104,12 @@ class ReportsSuite extends BaseSuite { reportsProvider.incognito.create( exampleReport("some_different_test_error_new") ) - val deleted = reportsProvider.incognito.cleanUpOldReports(2) + val deleted = reportsProvider.incognito.cleanUpOldReports(2).asScala.toList deleted match { case (_ :: _ :: Nil) if deleted.forall(_.name.contains("old")) => case _ => fail(s"deleted: ${deleted.map(_.name)}") } - val reports = reportsProvider.incognito.getReports() + val reports = reportsProvider.incognito.getReports().asScala.toList reports match { case (_ :: _ :: Nil) if reports.forall(_.name.contains("new")) => case _ => fail(s"reports: ${reports.map(_.name)}") @@ -117,7 +120,12 @@ class ReportsSuite extends BaseSuite { val testId = "test-id" val path = reportsProvider.incognito .create( - Report("test_error", exampleText(), "Test error", id = Some(testId)) + StandardReport( + "test_error", + exampleText(), + "Test error", + id = Some(testId), + ) ) .map(_.toRealPath()) val obtained = @@ -125,14 +133,19 @@ class ReportsSuite extends BaseSuite { assertNoDiff( s"""|error id: $testId |${exampleText(StdReportContext.WORKSPACE_STR)} - |#### Short summary: + |#### Short summary: | |Test error |""".stripMargin, obtained, ) val none1 = reportsProvider.incognito.create( - Report("test_error_again", exampleText(), "Test error", id = Some(testId)) + StandardReport( + "test_error_again", + exampleText(), + "Test error", + id = Some(testId), + ) ) assertEquals( none1.map(_.toRealPath()), @@ -141,10 +154,15 @@ class ReportsSuite extends BaseSuite { val newReportsProvider = new StdReportContext(workspace.toNIO, _ => Some("buildTarget")) val none2 = newReportsProvider.incognito.create( - Report("test_error_again", exampleText(), "Test error", id = Some(testId)) + StandardReport( + "test_error_again", + exampleText(), + "Test error", + id = Some(testId), + ) ) assertEquals(none2.map(_.toRealPath()), path) - val reports = newReportsProvider.incognito.getReports() + val reports = newReportsProvider.incognito.getReports().asScala.toList reports match { case head :: Nil => assert(head.file.getName == path.get.toFile.getName) case _ => fail(s"reports: ${reports.map(_.name)}") diff --git a/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala b/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala new file mode 100644 index 00000000000..b37b87781a7 --- /dev/null +++ b/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala @@ -0,0 +1,113 @@ +package tests.telemetry + +import java.{util => ju} + +import scala.util.Random.nextBoolean + +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.internal.telemetry + +object SampleReports { + private case class OptionalControl(setEmpty: Boolean) + private case class MapControl(setEmpty: Boolean) + private case class ListControl(setEmpty: Boolean) + + private def reportOf(ctx: telemetry.ReporterContext): telemetry.ErrorReport = + new telemetry.ErrorReport( + name = "name", + text = "text", + reporterContext = ctx, + id = Some("id"), + error = Some( + telemetry.ExceptionSummary( + "ExceptionType", + List( + telemetry.StackTraceElement( + "fullyQualifiedName", + "methodName", + "fileName", + 0, + ) + ), + ) + ), + reporterName = "reporterName", + env = telemetry.Environment( + new telemetry.JavaInfo("version", "distiribution"), + new telemetry.SystemInfo("arch", "name", "version"), + ), + ) + + private def presentationCompilerConfig() = + new telemetry.PresentationCompilerConfig( + Map.from(List("symbol" -> "prefix")), + Some("command"), + Some("parameterHints"), + "overrideDefFormat", + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + List("semanticDbOpts"), + ) + + def metalsLspReport(): telemetry.ErrorReport = { + reportOf( + telemetry.MetalsLspContext( + "metalsVersion", + telemetry.MetalsUserConfiguration( + Map.from(List("symbol" -> "prefix")), + nextBoolean(), + Some("bloopVersion"), + List("props"), + List("ammoniteProps"), + nextBoolean(), + Some("inferedTypes"), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + List("package.name"), + Some("fallback version"), + "testUserInterface", + ), + telemetry.MetalsServerConfiguration( + "clientCommand", + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + nextBoolean(), + presentationCompilerConfig, + ), + telemetry.MetalsClientInfo( + Some("name"), + Some("version"), + ), + List( + telemetry.BuildServerConnection( + "connection.name", + "connection.version", + util.Random.nextBoolean(), + ) + ), + ) + ) + } + + def scalaPresentationCompilerReport(): telemetry.ErrorReport = { + + reportOf( + new telemetry.ScalaPresentationCompilerContext( + "scalaVersion", + List("options", "othersOptions"), + presentationCompilerConfig(), + ) + ) + } +} diff --git a/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala b/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala new file mode 100644 index 00000000000..b94864b9cb0 --- /dev/null +++ b/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala @@ -0,0 +1,114 @@ +package tests.telemetry + +import scala.collection.mutable + +import scala.meta.internal.metals._ +import scala.meta.internal.pc.StandardReport +import scala.meta.internal.telemetry._ +import scala.meta.pc.Report + +import tests.BaseSuite +import tests.telemetry.SampleReports + +class TelemetryReporterSuite extends BaseSuite { + def simpleReport(id: String): Report = StandardReport( + name = "name", + text = "text", + shortSummary = "summmary", + path = None, + id = Some(id), + error = Some(new RuntimeException("A", new NullPointerException())), + ) + + def testSendError( + configuration: TelemetryConfiguration, + expected: Set[String], + ): Unit = + test( + s"Telemetry configuration: ${configuration} sends errors for ${expected + .mkString("(", ", ", ")")}" + ) { + val client = new TestTelemetryClient(configuration.telemetryLevel) + val reportContexts = Seq( + SampleReports.metalsLspReport(), + SampleReports.scalaPresentationCompilerReport(), + ).map(_.reporterContext) + + for { + reporterCtx <- reportContexts + reporter = new TelemetryReportContext( + telemetryConfiguration = () => configuration, + reporterContext = () => reporterCtx, + workspaceSanitizer = new WorkspaceSanitizer(None), + telemetryClient = client, + ) + } { + + reporter.incognito.create(simpleReport("incognito")) + reporter.bloop.create(simpleReport("bloop")) + reporter.unsanitized.create(simpleReport("unsanitized")) + + val received = client.reportsBuffer.map(_.id.get).toSet + assertEquals(received, expected) + } + } + + def testSendCrash( + telemetryLevel: TelemetryLevel, + expected: Set[String], + ): Unit = + test( + s"Telemetry configuration: ${telemetryLevel} sends crashes for ${expected + .mkString("(", ", ", ")")}" + ) { + val client = new TestTelemetryClient(telemetryLevel) + client.sendCrashReport( + CrashReport(ExceptionSummary("message", Nil), "testCrash") + ) + + val received = client.crashBuffer.map(_.componentName).toSet + assertEquals(received, expected) + } + + testSendError(TelemetryConfiguration(TelemetryLevel.Off, false), Set()) + testSendError(TelemetryConfiguration(TelemetryLevel.Off, true), Set()) + + testSendError(TelemetryConfiguration(TelemetryLevel.Crash, false), Set()) + testSendError(TelemetryConfiguration(TelemetryLevel.Crash, true), Set()) + + testSendError( + TelemetryConfiguration(TelemetryLevel.Error, false), + Set("incognito", "bloop"), + ) + testSendError( + TelemetryConfiguration(TelemetryLevel.Error, true), + Set("incognito", "bloop", "unsanitized"), + ) + + testSendError( + TelemetryConfiguration(TelemetryLevel.All, false), + Set("incognito", "bloop"), + ) + testSendError( + TelemetryConfiguration(TelemetryLevel.All, true), + Set("incognito", "bloop", "unsanitized"), + ) + + testSendCrash(TelemetryLevel.Off, Set()) + testSendCrash(TelemetryLevel.Crash, Set("testCrash")) + testSendCrash(TelemetryLevel.Error, Set("testCrash")) + testSendCrash(TelemetryLevel.All, Set("testCrash")) + +} + +class TestTelemetryClient(val telemetryLevel: TelemetryLevel) + extends TelemetryClient { + val reportsBuffer = mutable.ListBuffer.empty[ErrorReport] + val crashBuffer = mutable.ListBuffer.empty[CrashReport] + + override protected val sendErrorReportImpl: ErrorReport => Unit = error => + reportsBuffer += error + + override protected val sendCrashReportImpl: CrashReport => Unit = crash => + crashBuffer += crash +}