From 6f3d1042a6f4c69caac6edc30006a7258408c048 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 16 Jan 2025 19:22:19 +0100 Subject: [PATCH 1/9] chore: tmp --- etc/inspections.sh | 9 + etc/plugin-logs.sh | 3 + .../AbstractMongoDbInspectionBridge.kt | 29 ++- .../impl/FieldCheckInspectionBridge.kt | 49 ++++- .../impl/IndexCheckInspectionBridge.kt | 21 +- .../impl/NamespaceCheckInspectionBridge.kt | 61 +++++- .../jbplugin/observability/TelemetryEvent.kt | 36 ++++ .../probe/InspectionStatusChangedProbe.kt | 179 ++++++++++++++++++ .../fixtures/CodeInsightTestExtensions.kt | 7 +- ...avaDriverFieldCheckLinterInspectionTest.kt | 8 + ...avaDriverIndexCheckLinterInspectionTest.kt | 8 + ...riverNamespaceCheckLinterInspectionTest.kt | 8 + .../javadriver/JavaDriverRepository.java | 2 +- 13 files changed, 389 insertions(+), 31 deletions(-) create mode 100755 etc/inspections.sh create mode 100755 etc/plugin-logs.sh create mode 100644 packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt diff --git a/etc/inspections.sh b/etc/inspections.sh new file mode 100755 index 00000000..ef349d23 --- /dev/null +++ b/etc/inspections.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +./etc/plugin-logs.sh |\ + grep 'inspection_id' |\ + jq '{ inspection: .inspection_type, id: .inspection_id, status: .error_status, meta: { error_field_type: .error_field_type, actual_field_type: .actual_field_type } }' |\ + jq -s |\ + jq 'group_by(.id) | map({ id: .[0].id, inspection: .[0].inspection, status: map(.status), meta: map(.meta) })' | + jq '.[]' + diff --git a/etc/plugin-logs.sh b/etc/plugin-logs.sh new file mode 100755 index 00000000..56692efd --- /dev/null +++ b/etc/plugin-logs.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cat ./packages/jetbrains-plugin/build/idea-sandbox/system/log/idea.log | grep "pluginVersion" diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/AbstractMongoDbInspectionBridge.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/AbstractMongoDbInspectionBridge.kt index c3761b80..2a3246a7 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/AbstractMongoDbInspectionBridge.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/AbstractMongoDbInspectionBridge.kt @@ -30,12 +30,19 @@ abstract class AbstractMongoDbInspectionBridge( private val coroutineScope: CoroutineScope, private val inspection: MongoDbInspection, ) : AbstractBaseJavaLocalInspectionTool() { + protected abstract fun emitFinishedInspectionTelemetryEvent(problemsHolder: ProblemsHolder) + override fun buildVisitor( holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession, ): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitJavaFile(file: PsiJavaFile) { + super.visitJavaFile(file) + emitFinishedInspectionTelemetryEvent(holder) + } + override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { dispatchIfValidMongoDbQuery(expression) } @@ -53,8 +60,10 @@ abstract class AbstractMongoDbInspectionBridge( val dialect = expression.containingFile.dialect ?: return@runReadAction val queryService by expression.project.service() - queryService.queryAt(expression)?.let { query -> - fileInExpression.virtualFile?.let { + val query = queryService.queryAt(expression) + + if (query != null) { + if (fileInExpression.virtualFile != null) { inspection.visitMongoDbQuery( coroutineScope, dataSource?.localDataSource, @@ -62,13 +71,15 @@ abstract class AbstractMongoDbInspectionBridge( query, dialect.formatter, ) - } ?: inspection.visitMongoDbQuery( - coroutineScope, - null, - holder, - query, - dialect.formatter - ) + } else { + inspection.visitMongoDbQuery( + coroutineScope, + null, + holder, + query, + dialect.formatter + ) + } } } } diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/FieldCheckInspectionBridge.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/FieldCheckInspectionBridge.kt index 40d38500..a4e8bcd9 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/FieldCheckInspectionBridge.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/FieldCheckInspectionBridge.kt @@ -21,17 +21,29 @@ import com.mongodb.jbplugin.linting.FieldCheckingLinter import com.mongodb.jbplugin.meta.service import com.mongodb.jbplugin.mql.Node import com.mongodb.jbplugin.mql.components.HasCollectionReference +import com.mongodb.jbplugin.observability.TelemetryEvent +import com.mongodb.jbplugin.observability.probe.InspectionStatusChangedProbe import kotlinx.coroutines.CoroutineScope -/** - * @param coroutineScope - */ -@Suppress("MISSING_KDOC_TOP_LEVEL") class FieldCheckInspectionBridge(coroutineScope: CoroutineScope) : AbstractMongoDbInspectionBridge( coroutineScope, FieldCheckLinterInspection, - ) + ) { + override fun emitFinishedInspectionTelemetryEvent(problemsHolder: ProblemsHolder) { + val probe by service() + + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + problemsHolder + ) + + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.TYPE_MISMATCH, + problemsHolder + ) + } +} /** * This inspection object calls the linting engine and transforms the result so they can be rendered in the IntelliJ @@ -67,10 +79,11 @@ internal object FieldCheckLinterInspection : MongoDbInspection { is FieldCheckWarning.FieldDoesNotExist -> registerFieldDoesNotExistProblem( coroutineScope, problems, - it + it, + query ) is FieldCheckWarning.FieldValueTypeMismatch -> - registerFieldValueTypeMismatch(coroutineScope, problems, it, formatter) + registerFieldValueTypeMismatch(coroutineScope, problems, it, formatter, query) } } } @@ -121,6 +134,7 @@ internal object FieldCheckLinterInspection : MongoDbInspection { coroutineScope: CoroutineScope, problems: ProblemsHolder, warningInfo: FieldCheckWarning.FieldDoesNotExist, + query: Node, ) { val problemDescription = InspectionsAndInlaysMessages.message( "inspection.field.checking.error.message", @@ -138,6 +152,12 @@ internal object FieldCheckLinterInspection : MongoDbInspection { ), ), ) + + val probe by service() + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + query + ) } private fun registerFieldValueTypeMismatch( @@ -145,11 +165,15 @@ internal object FieldCheckLinterInspection : MongoDbInspection { problems: ProblemsHolder, warningInfo: FieldCheckWarning.FieldValueTypeMismatch, formatter: DialectFormatter, + query: Node ) { + val expectedType = formatter.formatType(warningInfo.fieldType) + val actualType = formatter.formatType(warningInfo.valueType) + val problemDescription = InspectionsAndInlaysMessages.message( "inspection.field.checking.error.message.value.type.mismatch", - formatter.formatType(warningInfo.valueType), - formatter.formatType(warningInfo.fieldType), + actualType, + expectedType, warningInfo.field, ) problems.registerProblem( @@ -163,5 +187,12 @@ internal object FieldCheckLinterInspection : MongoDbInspection { ), ), ) + + val probe by service() + probe.typeMismatchInspectionActive( + query, + actualType, + expectedType, + ) } } diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt index 651f6f60..6cfe68eb 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt @@ -21,17 +21,24 @@ import com.mongodb.jbplugin.linting.IndexCheckWarning import com.mongodb.jbplugin.linting.IndexCheckingLinter import com.mongodb.jbplugin.meta.service import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.observability.TelemetryEvent import com.mongodb.jbplugin.observability.probe.CreateIndexIntentionProbe +import com.mongodb.jbplugin.observability.probe.InspectionStatusChangedProbe import kotlinx.coroutines.CoroutineScope -/** - * @param coroutineScope - */ class IndexCheckInspectionBridge(coroutineScope: CoroutineScope) : AbstractMongoDbInspectionBridge( coroutineScope, IndexCheckLinterInspection, - ) + ) { + override fun emitFinishedInspectionTelemetryEvent(problemsHolder: ProblemsHolder) { + val probe by service() + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + problemsHolder, + ) + } +} /** * This inspection object calls the linting engine and transforms the result so they can be rendered in the IntelliJ @@ -70,6 +77,12 @@ internal object IndexCheckLinterInspection : MongoDbInspection { "inspection.index.checking.error.query.not.covered.by.index", ) + val probe by service() + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.QUERY_NOT_COVERED_BY_INDEX, + query + ) + problems.registerProblem( query.source, problemDescription, diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/NamespaceCheckInspectionBridge.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/NamespaceCheckInspectionBridge.kt index aed229bf..e4eb1947 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/NamespaceCheckInspectionBridge.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/NamespaceCheckInspectionBridge.kt @@ -20,17 +20,36 @@ import com.mongodb.jbplugin.linting.NamespaceCheckWarning import com.mongodb.jbplugin.linting.NamespaceCheckingLinter import com.mongodb.jbplugin.meta.service import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.observability.TelemetryEvent +import com.mongodb.jbplugin.observability.probe.InspectionStatusChangedProbe import kotlinx.coroutines.CoroutineScope /** * @param coroutineScope */ -@Suppress("MISSING_KDOC_TOP_LEVEL") class NamespaceCheckInspectionBridge(coroutineScope: CoroutineScope) : AbstractMongoDbInspectionBridge( coroutineScope, NamespaceCheckingLinterInspection, - ) + ) { + override fun emitFinishedInspectionTelemetryEvent(problemsHolder: ProblemsHolder) { + val probe by service() + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.NO_NAMESPACE_INFERRED, + problemsHolder + ) + + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.DATABASE_DOES_NOT_EXIST, + problemsHolder + ) + + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.COLLECTION_DOES_NOT_EXIST, + problemsHolder + ) + } +} /** * This inspection object calls the linting engine and transforms the result so they can be rendered in the IntelliJ @@ -59,17 +78,24 @@ internal object NamespaceCheckingLinterInspection : MongoDbInspection { result.warnings.forEach { when (it) { is NamespaceCheckWarning.NoNamespaceInferred -> - registerNoNamespaceInferred(coroutineScope, problems, it.source) + registerNoNamespaceInferred(coroutineScope, problems, it.source, query) is NamespaceCheckWarning.CollectionDoesNotExist -> registerCollectionDoesNotExist( coroutineScope, problems, it.source, it.database, - it.collection + it.collection, + query ) is NamespaceCheckWarning.DatabaseDoesNotExist -> - registerDatabaseDoesNotExist(coroutineScope, problems, it.source, it.database) + registerDatabaseDoesNotExist( + coroutineScope, + problems, + it.source, + it.database, + query + ) } } } @@ -77,8 +103,15 @@ internal object NamespaceCheckingLinterInspection : MongoDbInspection { private fun registerNoNamespaceInferred( coroutineScope: CoroutineScope, problems: ProblemsHolder, - source: PsiElement + source: PsiElement, + query: Node ) { + val probe by service() + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.NO_NAMESPACE_INFERRED, + query + ) + val problemDescription = InspectionsAndInlaysMessages.message( "inspection.namespace.checking.error.message", ) @@ -100,7 +133,14 @@ internal object NamespaceCheckingLinterInspection : MongoDbInspection { problems: ProblemsHolder, source: PsiElement, dbName: String, + query: Node ) { + val probe by service() + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.DATABASE_DOES_NOT_EXIST, + query + ) + val problemDescription = InspectionsAndInlaysMessages.message( "inspection.namespace.checking.error.message.database.missing", dbName @@ -123,8 +163,15 @@ internal object NamespaceCheckingLinterInspection : MongoDbInspection { problems: ProblemsHolder, source: PsiElement, dbName: String, - collName: String + collName: String, + query: Node ) { + val probe by service() + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.COLLECTION_DOES_NOT_EXIST, + query + ) + val problemDescription = InspectionsAndInlaysMessages.message( "inspection.namespace.checking.error.message.collection.missing", collName, diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/TelemetryEvent.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/TelemetryEvent.kt index e4bbbaf2..5335ce0c 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/TelemetryEvent.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/TelemetryEvent.kt @@ -40,6 +40,10 @@ enum class TelemetryProperty( CONSOLE("console"), TRIGGER_LOCATION("trigger_location"), SIGNAL_TYPE("signal_type"), + INSPECTION_TYPE("inspection_type"), + ERROR_FIELD_TYPE("error_field_type"), + ACTUAL_ERROR_TYPE("actual_field_type"), + ERROR_STATUS("error_status") } enum class SignalType( @@ -227,4 +231,36 @@ sealed class TelemetryEvent( TelemetryProperty.SIGNAL_TYPE to SignalType.MISSING_INDEX.publicName, ) ) + + class InspectionStatusChangeEvent( + dialect: HasSourceDialect.DialectName, + inspectionType: InspectionType, + inspectionStatus: InspectionStatus, + actualFieldType: String?, + expectedFieldType: String?, + ) : TelemetryEvent( + name = "Inspection", + properties = + mapOf( + TelemetryProperty.DIALECT to dialect.name.lowercase(), + TelemetryProperty.INSPECTION_TYPE to inspectionType.name.lowercase(), + TelemetryProperty.ERROR_STATUS to inspectionStatus.name.lowercase(), + TelemetryProperty.ERROR_FIELD_TYPE to (actualFieldType ?: ""), + TelemetryProperty.ACTUAL_ERROR_TYPE to (expectedFieldType ?: ""), + ) + ) { + enum class InspectionType { + FIELD_DOES_NOT_EXIST, + TYPE_MISMATCH, + QUERY_NOT_COVERED_BY_INDEX, + NO_NAMESPACE_INFERRED, + COLLECTION_DOES_NOT_EXIST, + DATABASE_DOES_NOT_EXIST + } + + enum class InspectionStatus { + ACTIVE, + RESOLVED + } + } } diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt new file mode 100644 index 00000000..31c98246 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -0,0 +1,179 @@ +package com.mongodb.jbplugin.observability.probe + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.database.util.common.containsElements +import com.intellij.openapi.application.readAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.logger +import com.intellij.psi.PsiElement +import com.mongodb.jbplugin.meta.service +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.components.HasSourceDialect +import com.mongodb.jbplugin.observability.TelemetryEvent +import com.mongodb.jbplugin.observability.TelemetryEvent.InspectionStatusChangeEvent.InspectionType +import com.mongodb.jbplugin.observability.TelemetryService +import com.mongodb.jbplugin.observability.useLogMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference +import java.util.Collections +import java.util.UUID + +private val inspectionTelemetry = Dispatchers.IO +private val logger: Logger = logger() + +@Service +class InspectionStatusChangedProbe( + private val cs: CoroutineScope +) { + private data class UniqueInspection(val id: UUID, val on: WeakReference>) { + companion object { + fun new(query: Node): UniqueInspection { + return UniqueInspection(UUID.randomUUID(), WeakReference(query)) + } + } + } + + private val problemsByInspectionType: MutableMap> = + Collections.synchronizedMap(mutableMapOf()) + + fun inspectionChanged(inspectionType: InspectionType, query: Node) { + val dialect = query.component() ?: return + val psiElement = query.source + + cs.launch(inspectionTelemetry) { + val elementsWithProblems = problemsByInspectionType(inspectionType) + + // check if the element is already in the list + if (isElementRegistered(elementsWithProblems, psiElement)) { + // do nothing, it's already registered + return@launch + } + + // it's a new error, send a telemetry event and store it + val inspection = UniqueInspection.new(query) + elementsWithProblems.add(inspection) + + val telemetry by service() + val event = TelemetryEvent.InspectionStatusChangeEvent( + dialect = dialect.name, + inspectionType = inspectionType, + inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, + null, + null + ) + + telemetry.sendEvent(event) + logger.info( + useLogMessage("New inspection triggered") + .put("inspection_id", inspection.id.toString()) + .mergeTelemetryEventProperties(event) + .build() + ) + + problemsByInspectionType[inspectionType] = elementsWithProblems + } + } + + fun typeMismatchInspectionActive(query: Node, actualType: String, expectedType: String) { + val inspectionType = InspectionType.TYPE_MISMATCH + val dialect = query.component() ?: return + val psiElement = query.source + + cs.launch(inspectionTelemetry) { + val elementsWithProblems = problemsByInspectionType(inspectionType) + + if (isElementRegistered(elementsWithProblems, psiElement)) { + // do nothing, it's already registered + return@launch + } + + // it's a new error, send a telemetry event and store it + val inspection = UniqueInspection.new(query) + elementsWithProblems.add(inspection) + + val telemetry by service() + val event = TelemetryEvent.InspectionStatusChangeEvent( + dialect = dialect.name, + inspectionType = inspectionType, + inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, + actualFieldType = actualType, + expectedFieldType = expectedType, + ) + + telemetry.sendEvent(event) + logger.info( + useLogMessage("New inspection triggered") + .put("inspection_id", inspection.id.toString()) + .mergeTelemetryEventProperties(event) + .build() + ) + + problemsByInspectionType[inspectionType] = elementsWithProblems + } + } + + fun finishedProcessingInspections(inspectionType: InspectionType, problemsHolder: ProblemsHolder) { + cs.launch(inspectionTelemetry) { + val elementsWithProblems = problemsByInspectionType(inspectionType) + + val results = problemsHolder.results + // check all our registered problems + // if at the end of the processing cycle it's empty + // we will assume they are + for (loopResult in results) { + for ((idx, elementWithProblem) in elementsWithProblems.withIndex()) { + if (isElementRegistered(elementsWithProblems, loopResult.psiElement)) { + // the problem is still there, so don't do anything + // do nothing, it's already registered + break + } + + elementsWithProblems.removeAt(idx) + + val dialect = + elementWithProblem.on.get()?.component() ?: continue + val telemetry by service() + val event = TelemetryEvent.InspectionStatusChangeEvent( + dialect = dialect.name, + inspectionType = inspectionType, + inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.RESOLVED, + null, + null + ) + + telemetry.sendEvent(event) + logger.info( + useLogMessage("Inspection resolved") + .put("inspection_id", elementWithProblem.id.toString()) + .mergeTelemetryEventProperties(event) + .build() + ) + } + } + } + } + + private suspend fun isElementRegistered( + elementsWithProblems: MutableList, + psiElement: PsiElement + ): Boolean = runCatching { + readAction { + elementsWithProblems.containsElements { + it.on.get()?.source == psiElement || + it.on.get()?.source?.isEquivalentTo(psiElement) == true + } + } + }.getOrDefault(false) + + private fun problemsByInspectionType(inspectionType: InspectionType): MutableList { + val result = problemsByInspectionType.computeIfAbsent(inspectionType) { + Collections.synchronizedList(mutableListOf()) + } + + result.removeAll { it.on.get() == null } + return result + } +} diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/CodeInsightTestExtensions.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/CodeInsightTestExtensions.kt index 3873b4db..3ba4f756 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/CodeInsightTestExtensions.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/CodeInsightTestExtensions.kt @@ -11,6 +11,7 @@ import com.intellij.database.dataSource.LocalDataSource import com.intellij.database.dataSource.localDataSource import com.intellij.database.psi.DbDataSource import com.intellij.database.psi.DbPsiFacade +import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.module.Module @@ -37,6 +38,7 @@ import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory import com.mongodb.jbplugin.accessadapter.datagrip.DataGripBasedReadModelProvider import com.mongodb.jbplugin.dialects.Dialect import com.mongodb.jbplugin.editor.MongoDbVirtualFileDataSourceProvider +import com.mongodb.jbplugin.observability.TelemetryService import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import org.intellij.lang.annotations.Language @@ -135,6 +137,7 @@ internal class CodeInsightTestExtension : ).get(testFixtureKey) as CodeInsightTestFixture val dumbService = DumbService.getInstance(fixture.project) + ApplicationManager.getApplication().withMockedService(mock(TelemetryService::class.java)) // Run only when the code has been analysed runBlocking { suspendCancellableCoroutine { callback -> @@ -174,7 +177,8 @@ internal class CodeInsightTestExtension : parameterContext.parameter.type == Project::class.java || parameterContext.parameter.type == CodeInsightTestFixture::class.java || parameterContext.parameter.type == PsiFile::class.java || - parameterContext.parameter.type == JavaPsiFacade::class.java + parameterContext.parameter.type == JavaPsiFacade::class.java || + parameterContext.parameter.type == Application::class.java override fun resolveParameter( parameterContext: ParameterContext, @@ -189,6 +193,7 @@ internal class CodeInsightTestExtension : CodeInsightTestFixture::class.java -> fixture PsiFile::class.java -> fixture.file JavaPsiFacade::class.java -> JavaPsiFacade.getInstance(fixture.project) + Application::class.java -> ApplicationManager.getApplication() else -> TODO( "Parameter of type ${parameterContext.parameter.type.canonicalName} is not supported." ) diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt index b8fef1ca..cdb3e7d4 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt @@ -1,5 +1,6 @@ package com.mongodb.jbplugin.inspections.impl +import com.intellij.openapi.application.Application import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.mongodb.jbplugin.accessadapter.slice.GetCollectionSchema import com.mongodb.jbplugin.dialects.javadriver.glossary.JavaDriverDialect @@ -12,9 +13,11 @@ import com.mongodb.jbplugin.mql.BsonObject import com.mongodb.jbplugin.mql.BsonString import com.mongodb.jbplugin.mql.CollectionSchema import com.mongodb.jbplugin.mql.Namespace +import com.mongodb.jbplugin.observability.TelemetryService import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @CodeInsightTest class JavaDriverFieldCheckLinterInspectionTest { @@ -78,8 +81,11 @@ public class Repository { """, ) fun `shows an inspection when the field does not exist in the current namespace`( + app: Application, fixture: CodeInsightTestFixture, ) { + val telemetryService = app.getService(TelemetryService::class.java) + val (dataSource, readModelProvider) = fixture.setupConnection() fixture.specifyDialect(JavaDriverDialect) @@ -93,6 +99,8 @@ public class Repository { fixture.enableInspections(FieldCheckInspectionBridge::class.java) fixture.testHighlighting() + + verify(telemetryService).sendEvent(any()) } @ParsingTest( diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt index 23c6adee..e03a6dce 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt @@ -1,5 +1,6 @@ package com.mongodb.jbplugin.inspections.impl +import com.intellij.openapi.application.Application import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.mongodb.jbplugin.accessadapter.ExplainPlan import com.mongodb.jbplugin.accessadapter.slice.ExplainQuery @@ -8,9 +9,11 @@ import com.mongodb.jbplugin.fixtures.CodeInsightTest import com.mongodb.jbplugin.fixtures.ParsingTest import com.mongodb.jbplugin.fixtures.setupConnection import com.mongodb.jbplugin.fixtures.specifyDialect +import com.mongodb.jbplugin.observability.TelemetryService import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @CodeInsightTest @Suppress("TOO_LONG_FUNCTION", "LONG_LINE") @@ -43,8 +46,11 @@ public class Repository { """, ) fun `shows an inspection when the query is a collscan`( + app: Application, fixture: CodeInsightTestFixture, ) { + val telemetryService = app.getService(TelemetryService::class.java) + val (dataSource, readModelProvider) = fixture.setupConnection() fixture.specifyDialect(JavaDriverDialect) @@ -54,5 +60,7 @@ public class Repository { fixture.enableInspections(IndexCheckInspectionBridge::class.java) fixture.testHighlighting() + + verify(telemetryService).sendEvent(any()) } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt index 9b154dcd..35f3f872 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt @@ -1,5 +1,6 @@ package com.mongodb.jbplugin.inspections.impl +import com.intellij.openapi.application.Application import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.mongodb.jbplugin.accessadapter.slice.ListCollections import com.mongodb.jbplugin.accessadapter.slice.ListDatabases @@ -8,9 +9,11 @@ import com.mongodb.jbplugin.fixtures.CodeInsightTest import com.mongodb.jbplugin.fixtures.ParsingTest import com.mongodb.jbplugin.fixtures.setupConnection import com.mongodb.jbplugin.fixtures.specifyDialect +import com.mongodb.jbplugin.observability.TelemetryService import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @CodeInsightTest @Suppress("TOO_LONG_FUNCTION", "LONG_LINE") @@ -80,8 +83,11 @@ public class Repository { """, ) fun `shows an inspection when the collection does not exist in the current data source`( + app: Application, fixture: CodeInsightTestFixture, ) { + val telemetryService = app.getService(TelemetryService::class.java) + val (dataSource, readModelProvider) = fixture.setupConnection() fixture.specifyDialect(JavaDriverDialect) @@ -95,5 +101,7 @@ public class Repository { fixture.enableInspections(NamespaceCheckInspectionBridge::class.java) fixture.testHighlighting() + + verify(telemetryService).sendEvent(any()) } } diff --git a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java index 5e8dc079..4d897b23 100644 --- a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java +++ b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java @@ -30,7 +30,7 @@ public Document findMovieById(String id) { .first(); } - public List findMoviesByYear(String year) { + public List findMoviesByYear(int year) { return client .getDatabase("sample_mflix") .getCollection("movies") From cb7b55897c557b50499efb535d49ef4fcbea53c8 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 16 Jan 2025 19:50:47 +0100 Subject: [PATCH 2/9] chore: add error message when querying mongodb fails --- .../probe/InspectionStatusChangedProbe.kt | 17 +++++++++-------- .../javadriver/JavaDriverRepository.java | 2 +- .../datagrip/adapter/DataGripMongoDbDriver.kt | 14 +++++++++++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt index 31c98246..b71563b7 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.Collections import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList private val inspectionTelemetry = Dispatchers.IO private val logger: Logger = logger() @@ -47,7 +48,7 @@ class InspectionStatusChangedProbe( val elementsWithProblems = problemsByInspectionType(inspectionType) // check if the element is already in the list - if (isElementRegistered(elementsWithProblems, psiElement)) { + if (isElementRegistered(elementsWithProblems) { psiElement }) { // do nothing, it's already registered return@launch } @@ -85,7 +86,7 @@ class InspectionStatusChangedProbe( cs.launch(inspectionTelemetry) { val elementsWithProblems = problemsByInspectionType(inspectionType) - if (isElementRegistered(elementsWithProblems, psiElement)) { + if (isElementRegistered(elementsWithProblems) { psiElement }) { // do nothing, it's already registered return@launch } @@ -124,14 +125,14 @@ class InspectionStatusChangedProbe( // if at the end of the processing cycle it's empty // we will assume they are for (loopResult in results) { - for ((idx, elementWithProblem) in elementsWithProblems.withIndex()) { - if (isElementRegistered(elementsWithProblems, loopResult.psiElement)) { + for (elementWithProblem in elementsWithProblems) { + if (isElementRegistered(elementsWithProblems, loopResult::getPsiElement)) { // the problem is still there, so don't do anything // do nothing, it's already registered break } - elementsWithProblems.removeAt(idx) + elementsWithProblems.remove(elementWithProblem) val dialect = elementWithProblem.on.get()?.component() ?: continue @@ -158,19 +159,19 @@ class InspectionStatusChangedProbe( private suspend fun isElementRegistered( elementsWithProblems: MutableList, - psiElement: PsiElement + psiElement: () -> PsiElement ): Boolean = runCatching { readAction { elementsWithProblems.containsElements { it.on.get()?.source == psiElement || - it.on.get()?.source?.isEquivalentTo(psiElement) == true + it.on.get()?.source?.isEquivalentTo(psiElement()) == true } } }.getOrDefault(false) private fun problemsByInspectionType(inspectionType: InspectionType): MutableList { val result = problemsByInspectionType.computeIfAbsent(inspectionType) { - Collections.synchronizedList(mutableListOf()) + CopyOnWriteArrayList() } result.removeAll { it.on.get() == null } diff --git a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java index 4d897b23..31d5d24e 100644 --- a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java +++ b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java @@ -48,7 +48,7 @@ public Document queryMovieById(String id) { .first(); } - public List queryMoviesByYear(String year) { + public List queryMoviesByYear(int year) { return client .getDatabase("sample_mflix") .getCollection("movies") diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt index 44645c3c..4bb302b3 100644 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt @@ -12,6 +12,8 @@ import com.intellij.database.dataSource.LocalDataSource import com.intellij.database.dataSource.connection.ConnectionRequestor import com.intellij.database.run.ConsoleRunConfiguration import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.mongodb.ConnectionString import com.mongodb.MongoClientSettings @@ -44,6 +46,8 @@ private const val TIMEOUT = 5 @OptIn(ExperimentalCoroutinesApi::class) private val mongosh = Dispatchers.IO.limitedParallelism(1) +private val logger: Logger = logger() + /** * The driver itself. Shouldn't be used directly, but through the * DataGripBasedReadModelProvider. @@ -206,8 +210,16 @@ internal class DataGripMongoDbDriver( withTimeout(timeout) { val listOfResults = mutableListOf() - val resultSet = statement.executeQuery() ?: return@withTimeout emptyList() + val queryResult = runCatching { statement.executeQuery() } + if (queryResult.isFailure) { + logger.error( + "Can not query MongoDB: $queryString", + queryResult.exceptionOrNull() + ) + return@withTimeout emptyList() + } + val resultSet = queryResult.getOrNull() ?: return@withTimeout emptyList() if (resultClass.java == Unit::class.java) { listOfResults.add(Unit as T) return@withTimeout listOfResults From ace3453280adcc18d040c0001a02abda2d975ba2 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 16 Jan 2025 21:57:59 +0100 Subject: [PATCH 3/9] chore: add to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2bca3b1..90c49639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ MongoDB plugin for IntelliJ IDEA. ## [Unreleased] ### Added +* [INTELLIJ-180](https://jira.mongodb.org/browse/INTELLIJ-180) Telemetry when inspections are shown and resolved. + It can be disabled in the Plugin settings. * [INTELLIJ-173](https://jira.mongodb.org/browse/INTELLIJ-173) Add support for parsing, inspecting and autocompleting in a project stage written using `Aggregation.match` and chained `ProjectionOperations` using `andInclude` and `andExclude`. * [INTELLIJ-172](https://jira.mongodb.org/browse/INTELLIJ-172) Add support for parsing, inspecting and autocompleting in an aggregation written using Spring Data MongoDB (`MongoTemplate.aggregate`, `MongoTemplate.aggregateStream`) and a match stage written using `Aggregation.match`. * [INTELLIJ-179](https://jira.mongodb.org/browse/INTELLIJ-179) Telemetry when Create Index intention is clicked. From e26d6638a6378185a71567f649186842183ac7ab Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 16 Jan 2025 21:59:11 +0100 Subject: [PATCH 4/9] chore: remove refs to deprecated methods --- .../observability/probe/InspectionStatusChangedProbe.kt | 5 ++--- .../dialects/springquery/SpringAtQueryDialectParser.kt | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt index b71563b7..753e0533 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -1,7 +1,6 @@ package com.mongodb.jbplugin.observability.probe import com.intellij.codeInspection.ProblemsHolder -import com.intellij.database.util.common.containsElements import com.intellij.openapi.application.readAction import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger @@ -162,10 +161,10 @@ class InspectionStatusChangedProbe( psiElement: () -> PsiElement ): Boolean = runCatching { readAction { - elementsWithProblems.containsElements { + elementsWithProblems.find { it.on.get()?.source == psiElement || it.on.get()?.source?.isEquivalentTo(psiElement()) == true - } + } != null } }.getOrDefault(false) diff --git a/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt b/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt index 8b4a89d6..0a08962b 100644 --- a/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt +++ b/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt @@ -1,6 +1,5 @@ package com.mongodb.jbplugin.dialects.springquery -import com.intellij.database.util.common.containsElements import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation @@ -272,9 +271,9 @@ object SpringAtQueryDialectParser : DialectParser { private fun findParentMethodWithQueryAnnotation(source: PsiElement): PsiMethod? { return source.findTopParentBy { method -> method as? PsiMethod ?: return@findTopParentBy false - method.annotations.containsElements { + method.annotations.find { it.hasQualifiedName(QUERY_FQN) - } + } != null } as? PsiMethod } From 1493e93540d80fae41722b2577c62bbe8495ac28 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 16 Jan 2025 22:41:13 +0100 Subject: [PATCH 5/9] chore: fix finished processing inspections, now it tracks resolved inspections properly --- .../probe/InspectionStatusChangedProbe.kt | 149 +++++++++-------- .../probe/InspectionStatusChangedProbeTest.kt | 153 ++++++++++++++++++ 2 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbeTest.kt diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt index 753e0533..ebf05b16 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -1,7 +1,7 @@ package com.mongodb.jbplugin.observability.probe import com.intellij.codeInspection.ProblemsHolder -import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.logger @@ -16,10 +16,10 @@ import com.mongodb.jbplugin.observability.useLogMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.lang.ref.WeakReference -import java.util.Collections import java.util.UUID -import java.util.concurrent.CopyOnWriteArrayList private val inspectionTelemetry = Dispatchers.IO private val logger: Logger = logger() @@ -36,8 +36,10 @@ class InspectionStatusChangedProbe( } } + private val mutex: Mutex = Mutex() + private val problemsByInspectionType: MutableMap> = - Collections.synchronizedMap(mutableMapOf()) + mutableMapOf() fun inspectionChanged(inspectionType: InspectionType, query: Node) { val dialect = query.component() ?: return @@ -46,34 +48,34 @@ class InspectionStatusChangedProbe( cs.launch(inspectionTelemetry) { val elementsWithProblems = problemsByInspectionType(inspectionType) - // check if the element is already in the list - if (isElementRegistered(elementsWithProblems) { psiElement }) { - // do nothing, it's already registered - return@launch - } + mutex.withLock { + // check if the element is already in the list + if (isElementRegistered(elementsWithProblems) { psiElement }) { + // do nothing, it's already registered + return@launch + } - // it's a new error, send a telemetry event and store it - val inspection = UniqueInspection.new(query) - elementsWithProblems.add(inspection) - - val telemetry by service() - val event = TelemetryEvent.InspectionStatusChangeEvent( - dialect = dialect.name, - inspectionType = inspectionType, - inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, - null, - null - ) - - telemetry.sendEvent(event) - logger.info( - useLogMessage("New inspection triggered") - .put("inspection_id", inspection.id.toString()) - .mergeTelemetryEventProperties(event) - .build() - ) - - problemsByInspectionType[inspectionType] = elementsWithProblems + // it's a new error, send a telemetry event and store it + val inspection = UniqueInspection.new(query) + elementsWithProblems.add(inspection) + + val telemetry by service() + val event = TelemetryEvent.InspectionStatusChangeEvent( + dialect = dialect.name, + inspectionType = inspectionType, + inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, + null, + null + ) + + telemetry.sendEvent(event) + logger.info( + useLogMessage("New inspection triggered") + .put("inspection_id", inspection.id.toString()) + .mergeTelemetryEventProperties(event) + .build() + ) + } } } @@ -85,33 +87,33 @@ class InspectionStatusChangedProbe( cs.launch(inspectionTelemetry) { val elementsWithProblems = problemsByInspectionType(inspectionType) - if (isElementRegistered(elementsWithProblems) { psiElement }) { - // do nothing, it's already registered - return@launch - } + mutex.withLock { + if (isElementRegistered(elementsWithProblems) { psiElement }) { + // do nothing, it's already registered + return@launch + } - // it's a new error, send a telemetry event and store it - val inspection = UniqueInspection.new(query) - elementsWithProblems.add(inspection) - - val telemetry by service() - val event = TelemetryEvent.InspectionStatusChangeEvent( - dialect = dialect.name, - inspectionType = inspectionType, - inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, - actualFieldType = actualType, - expectedFieldType = expectedType, - ) - - telemetry.sendEvent(event) - logger.info( - useLogMessage("New inspection triggered") - .put("inspection_id", inspection.id.toString()) - .mergeTelemetryEventProperties(event) - .build() - ) - - problemsByInspectionType[inspectionType] = elementsWithProblems + // it's a new error, send a telemetry event and store it + val inspection = UniqueInspection.new(query) + elementsWithProblems.add(inspection) + + val telemetry by service() + val event = TelemetryEvent.InspectionStatusChangeEvent( + dialect = dialect.name, + inspectionType = inspectionType, + inspectionStatus = TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, + actualFieldType = actualType, + expectedFieldType = expectedType, + ) + + telemetry.sendEvent(event) + logger.info( + useLogMessage("New inspection triggered") + .put("inspection_id", inspection.id.toString()) + .mergeTelemetryEventProperties(event) + .build() + ) + } } } @@ -123,12 +125,15 @@ class InspectionStatusChangedProbe( // check all our registered problems // if at the end of the processing cycle it's empty // we will assume they are - for (loopResult in results) { + mutex.withLock { for (elementWithProblem in elementsWithProblems) { - if (isElementRegistered(elementsWithProblems, loopResult::getPsiElement)) { + val findEquivalentProblem = results.find { + isElementRegistered(elementsWithProblems, it::getPsiElement) + } + if (findEquivalentProblem != null) { // the problem is still there, so don't do anything // do nothing, it's already registered - break + continue } elementsWithProblems.remove(elementWithProblem) @@ -156,24 +161,28 @@ class InspectionStatusChangedProbe( } } - private suspend fun isElementRegistered( + private fun isElementRegistered( elementsWithProblems: MutableList, psiElement: () -> PsiElement ): Boolean = runCatching { - readAction { + ApplicationManager.getApplication().runReadAction { elementsWithProblems.find { - it.on.get()?.source == psiElement || - it.on.get()?.source?.isEquivalentTo(psiElement()) == true + val isStrictlyEqual = it.on.get()?.source == psiElement() + val isEquivalent = it.on.get()?.source?.isEquivalentTo(psiElement()) == true + + isStrictlyEqual || isEquivalent } != null } }.getOrDefault(false) - private fun problemsByInspectionType(inspectionType: InspectionType): MutableList { - val result = problemsByInspectionType.computeIfAbsent(inspectionType) { - CopyOnWriteArrayList() - } + private suspend fun problemsByInspectionType(inspectionType: InspectionType): MutableList { + return mutex.withLock { + val result = problemsByInspectionType.computeIfAbsent(inspectionType) { + mutableListOf() + } - result.removeAll { it.on.get() == null } - return result + result.removeAll { it.on.get() == null } + result + } } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbeTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbeTest.kt new file mode 100644 index 00000000..02996d92 --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbeTest.kt @@ -0,0 +1,153 @@ +package com.mongodb.jbplugin.observability.probe + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.application.Application +import com.intellij.psi.PsiElement +import com.mongodb.jbplugin.fixtures.IntegrationTest +import com.mongodb.jbplugin.fixtures.mockLogMessage +import com.mongodb.jbplugin.fixtures.withMockedService +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.components.HasSourceDialect +import com.mongodb.jbplugin.observability.TelemetryEvent +import com.mongodb.jbplugin.observability.TelemetryService +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.jupiter.api.Test +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.timeout +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@IntegrationTest +internal class InspectionStatusChangedProbeTest { + @Test + fun `should send a InspectionStatusChangeEvent event when found for the first time`( + application: Application, + testScope: TestScope + ) { + val telemetryService = mock() + val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random() + + val query = Node(null, listOf(HasSourceDialect(dialect))) as Node + + application.withMockedService(telemetryService) + .withMockedService(mockLogMessage()) + + val probe = InspectionStatusChangedProbe(testScope) + + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + query + ) + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + query + ) + + testScope.advanceUntilIdle() + + verify(telemetryService, timeout(1000).times(1)).sendEvent(any()) + } + + @Test + fun `should send a InspectionStatusChangeEvent event when multiple types of events`( + application: Application, + testScope: TestScope + ) { + val telemetryService = mock() + val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random() + + val query = Node(null, listOf(HasSourceDialect(dialect))) as Node + + application.withMockedService(telemetryService) + .withMockedService(mockLogMessage()) + + val probe = InspectionStatusChangedProbe(testScope) + + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + query + ) + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + query + ) + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.NO_NAMESPACE_INFERRED, + query + ) + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.NO_NAMESPACE_INFERRED, + query + ) + + testScope.advanceUntilIdle() + + verify(telemetryService, timeout(1000).times(2)).sendEvent(any()) + } + + @Test + fun `should send a InspectionStatusChangeEvent event with type checking issues`( + application: Application, + testScope: TestScope + ) { + val telemetryService = mock() + val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random() + + val query = Node(null, listOf(HasSourceDialect(dialect))) as Node + + application.withMockedService(telemetryService) + .withMockedService(mockLogMessage()) + + val probe = InspectionStatusChangedProbe(testScope) + + probe.typeMismatchInspectionActive(query, "actual", "expected") + + testScope.advanceUntilIdle() + + verify(telemetryService, timeout(1000).times(1)).sendEvent( + TelemetryEvent.InspectionStatusChangeEvent( + dialect, + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.TYPE_MISMATCH, + TelemetryEvent.InspectionStatusChangeEvent.InspectionStatus.ACTIVE, + "actual", + "expected" + ) + ) + } + + @Test + fun `should send a resolved InspectionStatusChangeEvent when there is no problem anymore`( + application: Application, + testScope: TestScope + ) { + val telemetryService = mock() + val problemsHolder = mock() + + val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random() + + `when`(problemsHolder.results).thenReturn(emptyList()) + + val query = Node(null, listOf(HasSourceDialect(dialect))) as Node + + application.withMockedService(telemetryService) + .withMockedService(mockLogMessage()) + + val probe = InspectionStatusChangedProbe(testScope) + + probe.inspectionChanged( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + query + ) + probe.finishedProcessingInspections( + TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST, + problemsHolder + ) + + testScope.advanceUntilIdle() + + verify(telemetryService, timeout(1000).times(2)).sendEvent(any()) + } +} From 0508c95fe16a8d19923ffc2cf80e2a71425ae4e7 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 16 Jan 2025 22:47:02 +0100 Subject: [PATCH 6/9] chore: fix linter --- .../observability/probe/InspectionStatusChangedProbe.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt index ebf05b16..a4d79f4a 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -12,6 +12,7 @@ import com.mongodb.jbplugin.mql.components.HasSourceDialect import com.mongodb.jbplugin.observability.TelemetryEvent import com.mongodb.jbplugin.observability.TelemetryEvent.InspectionStatusChangeEvent.InspectionType import com.mongodb.jbplugin.observability.TelemetryService +import com.mongodb.jbplugin.observability.probe.InspectionStatusChangedProbe.UniqueInspection import com.mongodb.jbplugin.observability.useLogMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -24,11 +25,13 @@ import java.util.UUID private val inspectionTelemetry = Dispatchers.IO private val logger: Logger = logger() +internal typealias ProblemsByInspectionType = MutableMap> + @Service class InspectionStatusChangedProbe( private val cs: CoroutineScope ) { - private data class UniqueInspection(val id: UUID, val on: WeakReference>) { + internal data class UniqueInspection(val id: UUID, val on: WeakReference>) { companion object { fun new(query: Node): UniqueInspection { return UniqueInspection(UUID.randomUUID(), WeakReference(query)) @@ -37,9 +40,7 @@ class InspectionStatusChangedProbe( } private val mutex: Mutex = Mutex() - - private val problemsByInspectionType: MutableMap> = - mutableMapOf() + private val problemsByInspectionType: ProblemsByInspectionType = mutableMapOf() fun inspectionChanged(inspectionType: InspectionType, query: Node) { val dialect = query.component() ?: return From 282f91dc84d0a4c76a63e7d7d363235e34497309 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Fri, 17 Jan 2025 15:13:53 +0100 Subject: [PATCH 7/9] chore: fix flaky tests --- .../probe/InspectionStatusChangedProbe.kt | 51 +++++++++---------- ...avaDriverFieldCheckLinterInspectionTest.kt | 3 +- ...avaDriverIndexCheckLinterInspectionTest.kt | 3 +- ...riverNamespaceCheckLinterInspectionTest.kt | 3 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt index a4d79f4a..ee2140e7 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.rd.util.launchChildNonUrgentBackground import com.intellij.psi.PsiElement import com.mongodb.jbplugin.meta.service import com.mongodb.jbplugin.mql.Node @@ -15,14 +16,12 @@ import com.mongodb.jbplugin.observability.TelemetryService import com.mongodb.jbplugin.observability.probe.InspectionStatusChangedProbe.UniqueInspection import com.mongodb.jbplugin.observability.useLogMessage import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import java.lang.ref.WeakReference -import java.util.UUID +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock -private val inspectionTelemetry = Dispatchers.IO private val logger: Logger = logger() internal typealias ProblemsByInspectionType = MutableMap> @@ -39,21 +38,21 @@ class InspectionStatusChangedProbe( } } - private val mutex: Mutex = Mutex() - private val problemsByInspectionType: ProblemsByInspectionType = mutableMapOf() + private val mutex: ReentrantLock = ReentrantLock() + private val problemsByInspectionType: ProblemsByInspectionType = ConcurrentHashMap() fun inspectionChanged(inspectionType: InspectionType, query: Node) { val dialect = query.component() ?: return val psiElement = query.source - cs.launch(inspectionTelemetry) { - val elementsWithProblems = problemsByInspectionType(inspectionType) - + cs.launchChildNonUrgentBackground { mutex.withLock { + val elementsWithProblems = problemsByInspectionType(inspectionType) + // check if the element is already in the list if (isElementRegistered(elementsWithProblems) { psiElement }) { // do nothing, it's already registered - return@launch + return@launchChildNonUrgentBackground } // it's a new error, send a telemetry event and store it @@ -85,13 +84,13 @@ class InspectionStatusChangedProbe( val dialect = query.component() ?: return val psiElement = query.source - cs.launch(inspectionTelemetry) { - val elementsWithProblems = problemsByInspectionType(inspectionType) - + cs.launchChildNonUrgentBackground { mutex.withLock { + val elementsWithProblems = problemsByInspectionType(inspectionType) + if (isElementRegistered(elementsWithProblems) { psiElement }) { // do nothing, it's already registered - return@launch + return@launchChildNonUrgentBackground } // it's a new error, send a telemetry event and store it @@ -119,14 +118,14 @@ class InspectionStatusChangedProbe( } fun finishedProcessingInspections(inspectionType: InspectionType, problemsHolder: ProblemsHolder) { - cs.launch(inspectionTelemetry) { - val elementsWithProblems = problemsByInspectionType(inspectionType) - + cs.launchChildNonUrgentBackground { val results = problemsHolder.results // check all our registered problems // if at the end of the processing cycle it's empty // we will assume they are mutex.withLock { + val elementsWithProblems = problemsByInspectionType(inspectionType) + for (elementWithProblem in elementsWithProblems) { val findEquivalentProblem = results.find { isElementRegistered(elementsWithProblems, it::getPsiElement) @@ -176,14 +175,12 @@ class InspectionStatusChangedProbe( } }.getOrDefault(false) - private suspend fun problemsByInspectionType(inspectionType: InspectionType): MutableList { - return mutex.withLock { - val result = problemsByInspectionType.computeIfAbsent(inspectionType) { - mutableListOf() - } - - result.removeAll { it.on.get() == null } - result + private fun problemsByInspectionType(inspectionType: InspectionType): MutableList { + val result = problemsByInspectionType.computeIfAbsent(inspectionType) { + Collections.synchronizedList(LinkedList()) } + + result.removeAll { it.on.get() == null } + return result } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt index cdb3e7d4..2aee036a 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt @@ -16,6 +16,7 @@ import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.observability.TelemetryService import org.mockito.Mockito.`when` import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq import org.mockito.kotlin.verify @@ -100,7 +101,7 @@ public class Repository { fixture.enableInspections(FieldCheckInspectionBridge::class.java) fixture.testHighlighting() - verify(telemetryService).sendEvent(any()) + verify(telemetryService, atLeastOnce()).sendEvent(any()) } @ParsingTest( diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt index e03a6dce..f92d6293 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverIndexCheckLinterInspectionTest.kt @@ -12,6 +12,7 @@ import com.mongodb.jbplugin.fixtures.specifyDialect import com.mongodb.jbplugin.observability.TelemetryService import org.mockito.Mockito.`when` import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq import org.mockito.kotlin.verify @@ -61,6 +62,6 @@ public class Repository { fixture.enableInspections(IndexCheckInspectionBridge::class.java) fixture.testHighlighting() - verify(telemetryService).sendEvent(any()) + verify(telemetryService, atLeastOnce()).sendEvent(any()) } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt index 35f3f872..aaffca20 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverNamespaceCheckLinterInspectionTest.kt @@ -12,6 +12,7 @@ import com.mongodb.jbplugin.fixtures.specifyDialect import com.mongodb.jbplugin.observability.TelemetryService import org.mockito.Mockito.`when` import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq import org.mockito.kotlin.verify @@ -102,6 +103,6 @@ public class Repository { fixture.enableInspections(NamespaceCheckInspectionBridge::class.java) fixture.testHighlighting() - verify(telemetryService).sendEvent(any()) + verify(telemetryService, atLeastOnce()).sendEvent(any()) } } From ec4f776fae0c42fde3ed19b802d2008faafd76c1 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Fri, 17 Jan 2025 16:04:54 +0100 Subject: [PATCH 8/9] chore: this test was a bit flaky --- .../probe/AutocompleteSuggestionAcceptedProbeTest.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/AutocompleteSuggestionAcceptedProbeTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/AutocompleteSuggestionAcceptedProbeTest.kt index 42e3b13e..f3f5ba77 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/AutocompleteSuggestionAcceptedProbeTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/observability/probe/AutocompleteSuggestionAcceptedProbeTest.kt @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.timeout import org.mockito.kotlin.verify @IntegrationTest @@ -44,7 +45,7 @@ class AutocompleteSuggestionAcceptedProbeTest { probe.sendEvents() - verify(telemetryService).sendEvent( + verify(telemetryService, timeout(1000)).sendEvent( TelemetryEvent.AutocompleteGroupEvent( HasSourceDialect.DialectName.JAVA_DRIVER, "database", @@ -53,7 +54,7 @@ class AutocompleteSuggestionAcceptedProbeTest { ), ) - verify(telemetryService).sendEvent( + verify(telemetryService, timeout(1000)).sendEvent( TelemetryEvent.AutocompleteGroupEvent( HasSourceDialect.DialectName.JAVA_DRIVER, "collection", @@ -62,7 +63,7 @@ class AutocompleteSuggestionAcceptedProbeTest { ), ) - verify(telemetryService).sendEvent( + verify(telemetryService, timeout(1000)).sendEvent( TelemetryEvent.AutocompleteGroupEvent( HasSourceDialect.DialectName.JAVA_DRIVER, "field", @@ -71,7 +72,7 @@ class AutocompleteSuggestionAcceptedProbeTest { ), ) - verify(telemetryService).sendEvent( + verify(telemetryService, timeout(1000)).sendEvent( TelemetryEvent.AutocompleteGroupEvent( HasSourceDialect.DialectName.JAVA_DRIVER, "field", @@ -80,7 +81,7 @@ class AutocompleteSuggestionAcceptedProbeTest { ), ) - verify(telemetryService).sendEvent( + verify(telemetryService, timeout(1000)).sendEvent( TelemetryEvent.AutocompleteGroupEvent( HasSourceDialect.DialectName.JAVA_DRIVER, "field", From 58ee88042e0d1d8a3386ed924b0a665cb77e2c5c Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Fri, 17 Jan 2025 16:20:54 +0100 Subject: [PATCH 9/9] chore: fail safe in case telemetry fails --- .../probe/InspectionStatusChangedProbe.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt index ee2140e7..523321e3 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -41,8 +42,19 @@ class InspectionStatusChangedProbe( private val mutex: ReentrantLock = ReentrantLock() private val problemsByInspectionType: ProblemsByInspectionType = ConcurrentHashMap() - fun inspectionChanged(inspectionType: InspectionType, query: Node) { - val dialect = query.component() ?: return + /** + * We are using this function because we don't really care much about exceptions here. It's just + * telemetry, and we will just fail safely and wait for the next event to happen. This is better + * than raising an exception to the user. + */ + private fun runSafe(fn: () -> Unit) { + runCatching { + fn() + }.getOrNull() + } + + fun inspectionChanged(inspectionType: InspectionType, query: Node) = runSafe { + val dialect = query.component() ?: return@runSafe val psiElement = query.source cs.launchChildNonUrgentBackground { @@ -79,9 +91,9 @@ class InspectionStatusChangedProbe( } } - fun typeMismatchInspectionActive(query: Node, actualType: String, expectedType: String) { + fun typeMismatchInspectionActive(query: Node, actualType: String, expectedType: String) = runSafe { val inspectionType = InspectionType.TYPE_MISMATCH - val dialect = query.component() ?: return + val dialect = query.component() ?: return@runSafe val psiElement = query.source cs.launchChildNonUrgentBackground { @@ -117,7 +129,7 @@ class InspectionStatusChangedProbe( } } - fun finishedProcessingInspections(inspectionType: InspectionType, problemsHolder: ProblemsHolder) { + fun finishedProcessingInspections(inspectionType: InspectionType, problemsHolder: ProblemsHolder) = runSafe { cs.launchChildNonUrgentBackground { val results = problemsHolder.results // check all our registered problems @@ -177,7 +189,7 @@ class InspectionStatusChangedProbe( private fun problemsByInspectionType(inspectionType: InspectionType): MutableList { val result = problemsByInspectionType.computeIfAbsent(inspectionType) { - Collections.synchronizedList(LinkedList()) + CopyOnWriteArrayList() } result.removeAll { it.on.get() == null }