diff --git a/CHANGELOG.md b/CHANGELOG.md index fa76beb5..737f15aa 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-176](https://jira.mongodb.org/browse/INTELLIJ-176) Add support for parsing, inspecting and autocompleting in an unwind stage written using `Aggregation.unwind`. * [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`. 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..523321e3 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/observability/probe/InspectionStatusChangedProbe.kt @@ -0,0 +1,198 @@ +package com.mongodb.jbplugin.observability.probe + +import com.intellij.codeInspection.ProblemsHolder +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 +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 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 + +private val logger: Logger = logger() + +internal typealias ProblemsByInspectionType = MutableMap> + +@Service +class InspectionStatusChangedProbe( + private val cs: CoroutineScope +) { + internal data class UniqueInspection(val id: UUID, val on: WeakReference>) { + companion object { + fun new(query: Node): UniqueInspection { + return UniqueInspection(UUID.randomUUID(), WeakReference(query)) + } + } + } + + private val mutex: ReentrantLock = ReentrantLock() + private val problemsByInspectionType: ProblemsByInspectionType = ConcurrentHashMap() + + /** + * 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 { + 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@launchChildNonUrgentBackground + } + + // 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() + ) + } + } + } + + fun typeMismatchInspectionActive(query: Node, actualType: String, expectedType: String) = runSafe { + val inspectionType = InspectionType.TYPE_MISMATCH + val dialect = query.component() ?: return@runSafe + val psiElement = query.source + + cs.launchChildNonUrgentBackground { + mutex.withLock { + val elementsWithProblems = problemsByInspectionType(inspectionType) + + if (isElementRegistered(elementsWithProblems) { psiElement }) { + // do nothing, it's already registered + return@launchChildNonUrgentBackground + } + + // 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() + ) + } + } + } + + fun finishedProcessingInspections(inspectionType: InspectionType, problemsHolder: ProblemsHolder) = runSafe { + 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) + } + if (findEquivalentProblem != null) { + // the problem is still there, so don't do anything + // do nothing, it's already registered + continue + } + + elementsWithProblems.remove(elementWithProblem) + + 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 fun isElementRegistered( + elementsWithProblems: MutableList, + psiElement: () -> PsiElement + ): Boolean = runCatching { + ApplicationManager.getApplication().runReadAction { + elementsWithProblems.find { + 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() + } + + 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..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 @@ -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,12 @@ 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.atLeastOnce import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @CodeInsightTest class JavaDriverFieldCheckLinterInspectionTest { @@ -78,8 +82,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 +100,8 @@ public class Repository { fixture.enableInspections(FieldCheckInspectionBridge::class.java) fixture.testHighlighting() + + 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 23c6adee..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 @@ -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,12 @@ 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.atLeastOnce import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @CodeInsightTest @Suppress("TOO_LONG_FUNCTION", "LONG_LINE") @@ -43,8 +47,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 +61,7 @@ public class Repository { fixture.enableInspections(IndexCheckInspectionBridge::class.java) fixture.testHighlighting() + + 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 9b154dcd..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 @@ -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,12 @@ 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.atLeastOnce import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @CodeInsightTest @Suppress("TOO_LONG_FUNCTION", "LONG_LINE") @@ -80,8 +84,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 +102,7 @@ public class Repository { fixture.enableInspections(NamespaceCheckInspectionBridge::class.java) fixture.testHighlighting() + + verify(telemetryService, atLeastOnce()).sendEvent(any()) } } 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", 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()) + } +} 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 7a8c91ac..e8499d6d 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") @@ -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 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 }