Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(telemetry): Add telemetry on signals INTELLIJ-180 #119

Merged
merged 11 commits into from
Jan 17, 2025
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
9 changes: 9 additions & 0 deletions etc/inspections.sh
Original file line number Diff line number Diff line change
@@ -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 '.[]'

3 changes: 3 additions & 0 deletions etc/plugin-logs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

cat ./packages/jetbrains-plugin/build/idea-sandbox/system/log/idea.log | grep "pluginVersion"
Original file line number Diff line number Diff line change
@@ -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,22 +60,26 @@ abstract class AbstractMongoDbInspectionBridge(
val dialect = expression.containingFile.dialect ?: return@runReadAction

val queryService by expression.project.service<CachedQueryService>()
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,
holder,
query,
dialect.formatter,
)
} ?: inspection.visitMongoDbQuery(
coroutineScope,
null,
holder,
query,
dialect.formatter
)
} else {
inspection.visitMongoDbQuery(
coroutineScope,
null,
holder,
query,
dialect.formatter
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<InspectionStatusChangedProbe>()

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<PsiElement>,
query: Node<PsiElement>,
) {
val problemDescription = InspectionsAndInlaysMessages.message(
"inspection.field.checking.error.message",
@@ -138,18 +152,28 @@ internal object FieldCheckLinterInspection : MongoDbInspection {
),
),
)

val probe by service<InspectionStatusChangedProbe>()
probe.inspectionChanged(
TelemetryEvent.InspectionStatusChangeEvent.InspectionType.FIELD_DOES_NOT_EXIST,
query
)
}

private fun registerFieldValueTypeMismatch(
coroutineScope: CoroutineScope,
problems: ProblemsHolder,
warningInfo: FieldCheckWarning.FieldValueTypeMismatch<PsiElement>,
formatter: DialectFormatter,
query: Node<PsiElement>
) {
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<InspectionStatusChangedProbe>()
probe.typeMismatchInspectionActive(
query,
actualType,
expectedType,
)
}
}
Original file line number Diff line number Diff line change
@@ -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<InspectionStatusChangedProbe>()
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<InspectionStatusChangedProbe>()
probe.inspectionChanged(
TelemetryEvent.InspectionStatusChangeEvent.InspectionType.QUERY_NOT_COVERED_BY_INDEX,
query
)

problems.registerProblem(
query.source,
problemDescription,
Original file line number Diff line number Diff line change
@@ -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<InspectionStatusChangedProbe>()
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,26 +78,40 @@ 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
)
}
}
}

private fun registerNoNamespaceInferred(
coroutineScope: CoroutineScope,
problems: ProblemsHolder,
source: PsiElement
source: PsiElement,
query: Node<PsiElement>
) {
val probe by service<InspectionStatusChangedProbe>()
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<PsiElement>
) {
val probe by service<InspectionStatusChangedProbe>()
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<PsiElement>
) {
val probe by service<InspectionStatusChangedProbe>()
probe.inspectionChanged(
TelemetryEvent.InspectionStatusChangeEvent.InspectionType.COLLECTION_DOES_NOT_EXIST,
query
)

val problemDescription = InspectionsAndInlaysMessages.message(
"inspection.namespace.checking.error.message.collection.missing",
collName,
Original file line number Diff line number Diff line change
@@ -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 ?: "<none>"),
TelemetryProperty.ACTUAL_ERROR_TYPE to (expectedFieldType ?: "<none>"),
)
) {
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<InspectionStatusChangedProbe>()

internal typealias ProblemsByInspectionType = MutableMap<InspectionType, MutableList<UniqueInspection>>

@Service
class InspectionStatusChangedProbe(
private val cs: CoroutineScope
) {
internal data class UniqueInspection(val id: UUID, val on: WeakReference<Node<PsiElement>>) {
companion object {
fun new(query: Node<PsiElement>): 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<PsiElement>) = runSafe {
val dialect = query.component<HasSourceDialect>() ?: 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<TelemetryService>()
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<PsiElement>, actualType: String, expectedType: String) = runSafe {
val inspectionType = InspectionType.TYPE_MISMATCH
val dialect = query.component<HasSourceDialect>() ?: 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<TelemetryService>()
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<HasSourceDialect>() ?: continue
val telemetry by service<TelemetryService>()
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<UniqueInspection>,
psiElement: () -> PsiElement
): Boolean = runCatching {
ApplicationManager.getApplication().runReadAction<Boolean> {
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<UniqueInspection> {
val result = problemsByInspectionType.computeIfAbsent(inspectionType) {
CopyOnWriteArrayList()
}

result.removeAll { it.on.get() == null }
return result
}
}
Original file line number Diff line number Diff line change
@@ -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."
)
Original file line number Diff line number Diff line change
@@ -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(
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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",
Original file line number Diff line number Diff line change
@@ -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<TelemetryService>()
val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random()

val query = Node<PsiElement?>(null, listOf(HasSourceDialect(dialect))) as Node<PsiElement>

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<TelemetryService>()
val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random()

val query = Node<PsiElement?>(null, listOf(HasSourceDialect(dialect))) as Node<PsiElement>

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<TelemetryService>()
val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random()

val query = Node<PsiElement?>(null, listOf(HasSourceDialect(dialect))) as Node<PsiElement>

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<TelemetryService>()
val problemsHolder = mock<ProblemsHolder>()

val dialect = HasSourceDialect.DialectName.entries.toTypedArray().random()

`when`(problemsHolder.results).thenReturn(emptyList())

val query = Node<PsiElement?>(null, listOf(HasSourceDialect(dialect))) as Node<PsiElement>

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())
}
}
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ public Document findMovieById(String id) {
.first();
}

public List<Document> findMoviesByYear(String year) {
public List<Document> findMoviesByYear(int year) {
return client
.getDatabase("sample_mflix")
.getCollection("movies")
@@ -48,7 +48,7 @@ public Document queryMovieById(String id) {
.first();
}

public List<Document> queryMoviesByYear(String year) {
public List<Document> queryMoviesByYear(int year) {
return client
.getDatabase("sample_mflix")
.getCollection("movies")
Original file line number Diff line number Diff line change
@@ -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<DataGripMongoDbDriver>()

/**
* The driver itself. Shouldn't be used directly, but through the
* DataGripBasedReadModelProvider.
@@ -206,8 +210,16 @@ internal class DataGripMongoDbDriver(

withTimeout(timeout) {
val listOfResults = mutableListOf<T>()
val resultSet = statement.executeQuery() ?: return@withTimeout emptyList()
val queryResult = runCatching { statement.executeQuery() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch 👁️

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
Original file line number Diff line number Diff line change
@@ -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<PsiElement> {
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
}