From 98f039706b6959a2644cc5f3bd00c3ee05a38888 Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Sat, 2 Dec 2023 21:17:15 +0100 Subject: [PATCH] Collect crash reports --- build.sbt | 1 - .../internal/metals/MetalsLspService.scala | 1 + .../main/scala/scala/meta/metals/Main.scala | 26 +++++++++++ .../meta/metals/MetalsLanguageServer.scala | 17 +++++++ .../internal/metals/TelemetryClient.scala | 27 ++++++++--- .../meta/internal/metals/TelemetryLevel.scala | 2 + .../metals/TelemetryReportContext.scala | 28 +++-------- .../pc/ScalaPresentationCompiler.scala | 1 - .../meta/internal/telemetry/CrashReport.java | 46 +++++++++++++++++++ .../meta/internal/telemetry/Environment.java | 24 ++++++---- .../{ReportEvent.java => ErrorReport.java} | 15 ++++-- ...portedError.java => ExceptionSummary.java} | 10 ++-- .../meta/internal/telemetry/JavaInfo.java | 6 +-- .../internal/telemetry/TelemetryService.java | 11 +++-- .../scala/tests/telemetry/SampleReports.scala | 12 ++--- .../telemetry/TelemetryReporterSuite.scala | 29 ++++++++---- 16 files changed, 186 insertions(+), 70 deletions(-) create mode 100644 telemetry-interface/src/main/java/scala/meta/internal/telemetry/CrashReport.java rename telemetry-interface/src/main/java/scala/meta/internal/telemetry/{ReportEvent.java => ErrorReport.java} (83%) rename telemetry-interface/src/main/java/scala/meta/internal/telemetry/{ReportedError.java => ExceptionSummary.java} (83%) diff --git a/build.sbt b/build.sbt index ed57b761512..28b62da1750 100644 --- a/build.sbt +++ b/build.sbt @@ -398,7 +398,6 @@ lazy val mtags3 = project Compile / unmanagedSourceDirectories += (ThisBuild / baseDirectory).value / "mtags-shared" / "src" / "main" / "scala-3", moduleName := "mtags3", scalaVersion := V.scala3, - crossScalaVersions := Seq(V.scala3), target := (ThisBuild / baseDirectory).value / "mtags" / "target" / "target3", publish / skip := true, scalafixConfig := Some( 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 98f39060eef..f1f5ffc109d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -150,6 +150,7 @@ class MetalsLspService( ThreadPools.discardRejectedRunnables("MetalsLanguageServer.ec", ec) def getVisibleName: String = folderVisibleName.getOrElse(folder.toString()) + def getTelemetryLevel: TelemetryLevel = userConfig.telemetryLevel private val cancelables = new MutableCancelable() val isCancelled = new AtomicBoolean(false) diff --git a/metals/src/main/scala/scala/meta/metals/Main.scala b/metals/src/main/scala/scala/meta/metals/Main.scala index a38b36922eb..0cf7f507995 100644 --- a/metals/src/main/scala/scala/meta/metals/Main.scala +++ b/metals/src/main/scala/scala/meta/metals/Main.scala @@ -1,6 +1,7 @@ package scala.meta.metals import java.util.concurrent.Executors +import java.util.Optional import scala.concurrent.ExecutionContext import scala.util.control.NonFatal @@ -10,6 +11,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.metals.TelemetryClient +import scala.meta.internal.telemetry.CrashReport +import scala.meta.internal.telemetry.ExceptionSummary import org.eclipse.lsp4j.jsonrpc.Launcher @@ -63,6 +67,7 @@ object Main { launcher.startListening().get() } catch { case NonFatal(e) => + trySendCrashReport(e, server) e.printStackTrace(systemOut) sys.exit(1) } finally { @@ -74,4 +79,25 @@ object Main { } } + private def trySendCrashReport( + error: Throwable, + server: MetalsLanguageServer, + ): Unit = try { + val telemetryLevel = server.getTelemetryLevel() + if (telemetryLevel.reportCrash) { + val telemetry = new TelemetryClient(() => telemetryLevel) + telemetry.sendCrashReport( + new CrashReport( + ExceptionSummary.fromThrowable(error, identity), + this.getClass().getName(), + Optional.of(BuildInfo.metalsVersion), + Optional.empty(), + ) + ) + } + } catch { + case err: Throwable => + System.err.println(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..c8ded65ff0a 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala @@ -19,6 +19,7 @@ import scala.meta.internal.metals.MutableCancelable import scala.meta.internal.metals.StdReportContext import scala.meta.internal.metals.ThreadPools import scala.meta.internal.metals.WorkspaceLspService +import scala.meta.internal.metals.TelemetryLevel import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.metals.clients.language.NoopLanguageClient import scala.meta.internal.metals.logging.MetalsLogger @@ -283,4 +284,20 @@ class MetalsLanguageServer( case _ => throw new IllegalStateException("Server is not initialized") } + private[metals] def getTelemetryLevel() = { + def maxConfiguredTelemetryLevel(service: WorkspaceLspService) = { + val entries = + service.workspaceFolders.getFolderServices.map(_.getTelemetryLevel) + if (entries.isEmpty) TelemetryLevel.default + else entries.max + } + serverState.get() match { + case ServerState.Initialized(service) => + maxConfiguredTelemetryLevel(service) + case ServerState.ShuttingDown(service) => + maxConfiguredTelemetryLevel(service) + case _ => TelemetryLevel.default + } + } + } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryClient.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryClient.scala index 80e73ca880d..a85d39fce54 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryClient.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryClient.scala @@ -4,6 +4,7 @@ import scala.meta.internal.telemetry import scala.util.Random import scala.util.Try +import scala.util.control.NonFatal import java.io.InputStreamReader @@ -79,7 +80,7 @@ object TelemetryClient { } } -private class TelemetryClient( +private[meta] class TelemetryClient( telemetryLevel: () => TelemetryLevel, config: TelemetryClient.Config = TelemetryClient.Config.default, logger: LoggerAccess = LoggerAccess.system @@ -89,12 +90,26 @@ private class TelemetryClient( implicit private def clientConfig: Config = config - private val SendReportEvent = new Endpoint(api.SendReportEventEndpoint) + private val SendErrorReport = new Endpoint(api.SendErrorReportEndpoint) + private val SendCrashReport = new Endpoint(api.SendCrashReportEndpoint) - override def sendReportEvent(event: telemetry.ReportEvent): Unit = + override def sendErrorReport(report: telemetry.ErrorReport): Unit = if (telemetryLevel().reportErrors) { - SendReportEvent(event).recover { case err => - logger.warning(s"Failed to send report: ${err}") - } + SendErrorReport(report) + .recover { case NonFatal(err) => + logSendFailure(reportType = "error")(err) + } + } + + override def sendCrashReport(report: telemetry.CrashReport): Unit = + if (telemetryLevel().reportCrash) { + SendCrashReport(report) + .recover { case NonFatal(err) => + logSendFailure(reportType = "crash")(err) + } } + + private def logSendFailure(reportType: String)(error: Throwable) = + logger.debug(s"Failed to send $reportType report: ${error}") + } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryLevel.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryLevel.scala index 807c20dc2fe..07547f08d6f 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryLevel.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryLevel.scala @@ -11,6 +11,8 @@ sealed class TelemetryLevel( } object TelemetryLevel { + implicit lazy val ordering: Ordering[TelemetryLevel] = Ordering.by(_.level) + case object Off extends TelemetryLevel(0, "off") case object Crash extends TelemetryLevel(1, "crash") case object Error extends TelemetryLevel(2, "error") diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryReportContext.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryReportContext.scala index 1d1121efa60..98818035b09 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryReportContext.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/TelemetryReportContext.scala @@ -78,32 +78,17 @@ private class TelemetryReporter( Nil override def deleteAll(): Unit = () - private lazy val environmentInfo: telemetry.Environment = { - def propertyOrUnknown(key: String) = sys.props.getOrElse(key, "unknown") - new telemetry.Environment( - /* java = */ new telemetry.JavaInfo( - /* version = */ propertyOrUnknown("java.version"), - /* distribution = */ sys.props.get("java.vendor").toJava - ), - /* system = */ new telemetry.SystemInfo( - /* architecture = */ propertyOrUnknown("os.arch"), - /* name = */ propertyOrUnknown("os.name"), - /* version = */ propertyOrUnknown("os.version") - ) - ) - } - override def sanitize(message: String): String = sanitizers.all.foldRight(message)(_.apply(_)) - private def createSanitizedReport(report: Report) = new telemetry.ReportEvent( + private def createSanitizedReport(report: Report) = new telemetry.ErrorReport( /* name = */ report.name, /* text = */ if (sanitizers.canSanitizeSources) Optional.of(sanitize(report.text)) else Optional.empty(), /* id = */ report.id.toJava, /* error = */ report.error - .map(telemetry.ReportedError.fromThrowable(_, sanitize(_))) + .map(telemetry.ExceptionSummary.fromThrowable(_, sanitize(_))) .toJava, /* reporterName = */ name, /* reporterContext = */ reporterContext() match { @@ -113,8 +98,7 @@ private class TelemetryReporter( telemetry.ReporterContextUnion.scalaPresentationCompiler(ctx) case ctx: telemetry.UnknownProducerContext => telemetry.ReporterContextUnion.unknown(ctx) - }, - /* env = */ environmentInfo + } ) override def create( @@ -122,9 +106,9 @@ private class TelemetryReporter( ifVerbose: Boolean ): Option[Path] = { if (telemetryLevel().reportErrors) { - val event = createSanitizedReport(unsanitizedReport) - if (event.getText().isPresent() || event.getError().isPresent()) - client.sendReportEvent(event) + val report = createSanitizedReport(unsanitizedReport) + if (report.getText().isPresent() || report.getError().isPresent()) + client.sendErrorReport(report) else logger.info( "Skiped reporting remotely unmeaningful report, no context or error, reportId=" + 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 7b26a468b42..5b819da047d 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 @@ -57,7 +57,6 @@ import org.eclipse.lsp4j.SelectionRange import org.eclipse.lsp4j.SignatureHelp import org.eclipse.lsp4j.TextEdit - case class ScalaPresentationCompiler( buildTargetIdentifier: String = "", buildTargetName: Option[String] = None, diff --git a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/CrashReport.java b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/CrashReport.java new file mode 100644 index 00000000000..a3eaffb4359 --- /dev/null +++ b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/CrashReport.java @@ -0,0 +1,46 @@ +package scala.meta.internal.telemetry; + +import java.util.Optional; + +public class CrashReport { + final private ExceptionSummary error; + final private String componentName; + final private Environment env; + final private Optional componentVersion; + final private Optional reporterContext; + + public CrashReport(ExceptionSummary error, String componentName, Optional componentVersion, + Optional reporterContext) { + this(error, componentName, Environment.get(), componentVersion, reporterContext); + } + + public CrashReport(ExceptionSummary error, String componentName, Environment env, Optional componentVersion, + Optional reporterContext) { + this.error = error; + this.componentName = componentName; + this.env = env; + this.componentVersion = componentVersion; + this.reporterContext = reporterContext; + } + + public ExceptionSummary getError() { + return error; + } + + public String getComponentName() { + return componentName; + } + + public Optional getComponentVersion() { + return componentVersion; + } + + public Optional getReporterContext() { + return reporterContext; + } + + public Environment getEnv() { + return env; + } + +} diff --git a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/Environment.java b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/Environment.java index d394a2de210..9713a8980f5 100644 --- a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/Environment.java +++ b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/Environment.java @@ -1,19 +1,27 @@ package scala.meta.internal.telemetry; public class Environment { - private JavaInfo java; - private SystemInfo system; + final private JavaInfo java; + final private SystemInfo system; - public Environment(JavaInfo java, SystemInfo system) { - this.java = java; - this.system = system; + private final static Environment instance; + + public static Environment get() { + return instance; } - public void setJava(JavaInfo java) { - this.java = java; + static { + instance = new Environment( + new JavaInfo(System.getProperty("java.version", "unknown"), + System.getProperty("java.vendor", "unknown")), + new SystemInfo(System.getProperty("os.arch", "unknown"), System.getProperty("os.name", "unknown"), + System.getProperty("os.version", "unknown"))); } - public void setSystem(SystemInfo system) { + // Generated + + public Environment(JavaInfo java, SystemInfo system) { + this.java = java; this.system = system; } diff --git a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ReportEvent.java b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ErrorReport.java similarity index 83% rename from telemetry-interface/src/main/java/scala/meta/internal/telemetry/ReportEvent.java rename to telemetry-interface/src/main/java/scala/meta/internal/telemetry/ErrorReport.java index f43e8a669f9..2a1295931dc 100644 --- a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ReportEvent.java +++ b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ErrorReport.java @@ -2,16 +2,21 @@ import java.util.Optional; -public class ReportEvent { +public class ErrorReport { final private String name; final private Optional text; final private Optional id; - final private Optional error; + final private Optional error; final private String reporterName; final private ReporterContextUnion reporterContext; final private Environment env; - public ReportEvent(String name, Optional text, Optional id, Optional error, + public ErrorReport(String name, Optional text, Optional id, Optional error, + String reporterName, ReporterContextUnion reporterContext) { + this(name, text, id, error, reporterName, reporterContext, Environment.get()); + } + + public ErrorReport(String name, Optional text, Optional id, Optional error, String reporterName, ReporterContextUnion reporterContext, Environment env) { this.name = name; this.text = text; @@ -34,7 +39,7 @@ public Optional getId() { return id; } - public Optional getError() { + public Optional getError() { return error; } @@ -72,7 +77,7 @@ public boolean equals(Object obj) { return false; if (getClass() != obj.getClass()) return false; - ReportEvent other = (ReportEvent) obj; + ErrorReport other = (ErrorReport) obj; if (name == null) { if (other.name != null) return false; diff --git a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ReportedError.java b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ExceptionSummary.java similarity index 83% rename from telemetry-interface/src/main/java/scala/meta/internal/telemetry/ReportedError.java rename to telemetry-interface/src/main/java/scala/meta/internal/telemetry/ExceptionSummary.java index 40f7503ec8c..9d17efc4b01 100644 --- a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ReportedError.java +++ b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/ExceptionSummary.java @@ -5,11 +5,11 @@ import java.util.List; import java.util.function.Function; -public class ReportedError { +public class ExceptionSummary { final private List exceptions; final private String stacktrace; - public ReportedError(List exceptions, String stacktrace) { + public ExceptionSummary(List exceptions, String stacktrace) { this.exceptions = exceptions; this.stacktrace = stacktrace; } @@ -23,7 +23,7 @@ public String getStacktrace() { } - public static ReportedError fromThrowable(Throwable exception, Function sanitizer) { + public static ExceptionSummary fromThrowable(Throwable exception, Function sanitizer) { List exceptions = new java.util.LinkedList<>(); for (Throwable current = exception; current != null; current = current.getCause()) { exceptions.add(current.getClass().getName()); @@ -33,7 +33,7 @@ public static ReportedError fromThrowable(Throwable exception, Function distribution; + final private String distribution; - public JavaInfo(String version, Optional distribution) { + public JavaInfo(String version, String distribution) { this.version = version; this.distribution = distribution; } @@ -15,7 +15,7 @@ public String getVersion() { return version; } - public Optional getDistribution() { + public String getDistribution() { return distribution; } diff --git a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/TelemetryService.java b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/TelemetryService.java index 1824ef99c95..b5e7206014a 100644 --- a/telemetry-interface/src/main/java/scala/meta/internal/telemetry/TelemetryService.java +++ b/telemetry-interface/src/main/java/scala/meta/internal/telemetry/TelemetryService.java @@ -2,8 +2,13 @@ public interface TelemetryService { - void sendReportEvent(ReportEvent event); + void sendErrorReport(ErrorReport report); - static final ServiceEndpoint SendReportEventEndpoint = new ServiceEndpoint<>("POST", - "/v1/telemetry/sendReportEvent", ReportEvent.class, Void.class); + static final ServiceEndpoint SendErrorReportEndpoint = new ServiceEndpoint<>("POST", + "/v1/telemetry/sendErrorReport", ErrorReport.class, Void.class); + + void sendCrashReport(CrashReport report); + + static final ServiceEndpoint SendCrashReportEndpoint = new ServiceEndpoint<>("POST", + "/v1/telemetry/sendCrashReport", CrashReport.class, Void.class); } diff --git a/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala b/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala index 8cc3f8b810e..6aa717de3b8 100644 --- a/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala +++ b/tests/unit/src/test/scala/tests/telemetry/SampleReports.scala @@ -28,12 +28,12 @@ object SampleReports { private def reportOf(ctx: telemetry.ReporterContextUnion)(implicit opt: OptionalControl, list: ListControl, - ): telemetry.ReportEvent = new telemetry.ReportEvent( + ): telemetry.ErrorReport = new telemetry.ErrorReport( "name", optional("text"), optional("id"), optional( - new telemetry.ReportedError( + new telemetry.ExceptionSummary( maybeEmptyList("ExceptionType"), "stacktrace", ) @@ -41,7 +41,7 @@ object SampleReports { "reporterName", ctx, new telemetry.Environment( - new telemetry.JavaInfo("version", optional("distiribution")), + new telemetry.JavaInfo("version", "distiribution"), new telemetry.SystemInfo("arch", "name", "version"), ), ) @@ -70,7 +70,7 @@ object SampleReports { emptyOptionals: Boolean = false, emptyLists: Boolean = false, emptyMaps: Boolean = false, - ): telemetry.ReportEvent = { + ): telemetry.ErrorReport = { implicit val ctrl: OptionalControl = OptionalControl(!emptyOptionals) implicit val map: MapControl = MapControl(!emptyMaps) implicit val list: ListControl = ListControl(!emptyLists) @@ -89,7 +89,7 @@ object SampleReports { emptyOptionals: Boolean = false, emptyLists: Boolean = false, emptyMaps: Boolean = false, - ): telemetry.ReportEvent = { + ): telemetry.ErrorReport = { implicit val ctrl: OptionalControl = OptionalControl(!emptyOptionals) implicit val map: MapControl = MapControl(!emptyMaps) implicit val list: ListControl = ListControl(!emptyLists) @@ -140,7 +140,7 @@ object SampleReports { emptyOptionals: Boolean = false, emptyLists: Boolean = false, emptyMaps: Boolean = false, - ): telemetry.ReportEvent = { + ): telemetry.ErrorReport = { implicit val ctrl: OptionalControl = OptionalControl(!emptyOptionals) implicit val map: MapControl = MapControl(!emptyMaps) implicit val list: ListControl = ListControl(!emptyLists) diff --git a/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala b/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala index 20582b64ea2..35fa1686673 100644 --- a/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala +++ b/tests/unit/src/test/scala/tests/telemetry/TelemetryReporterSuite.scala @@ -57,9 +57,7 @@ class TelemetryReporterSuite extends BaseSuite { // Test end-to-end connection and event serialization using local http server implementing TelemetryService endpoints test("connect with local server") { - implicit val ctx = new MockTelemetryServer.Context( - reports = mutable.ListBuffer.empty - ) + implicit val ctx = new MockTelemetryServer.Context() val server = MockTelemetryServer("127.0.0.1", 8081) server.start() try { @@ -83,7 +81,7 @@ class TelemetryReporterSuite extends BaseSuite { } { val createdReport = simpleReport(reporterCtx.toString()) reporter.incognito.create(createdReport) - val received = ctx.reports.filter(_.getId().toScala == createdReport.id) + val received = ctx.errors.filter(_.getId().toScala == createdReport.id) assert(received.nonEmpty, "Not received matching id") assert(received.size == 1, "Found more then 1 received event") } @@ -98,7 +96,12 @@ object MockTelemetryServer { import io.undertow.server.HttpServerExchange import io.undertow.util.Headers - case class Context(reports: mutable.ListBuffer[telemetry.ReportEvent]) + case class Context( + errors: mutable.ListBuffer[telemetry.ErrorReport] = + mutable.ListBuffer.empty, + crashes: mutable.ListBuffer[telemetry.CrashReport] = + mutable.ListBuffer.empty, + ) def apply( host: String, @@ -106,11 +109,17 @@ object MockTelemetryServer { )(implicit ctx: Context) = { val port = freePort(host, preferredPort) - val baseHandler = path().withEndpoint( - telemetry.TelemetryService.SendReportEventEndpoint, - defaultResponse = null.asInstanceOf[Void], - _.reports, - ) + val baseHandler = path() + .withEndpoint( + telemetry.TelemetryService.SendErrorReportEndpoint, + defaultResponse = null.asInstanceOf[Void], + _.errors, + ) + .withEndpoint( + telemetry.TelemetryService.SendCrashReportEndpoint, + defaultResponse = null.asInstanceOf[Void], + _.crashes, /*unused*/ + ) Undertow.builder .addHttpListener(port, host) .setHandler(baseHandler)