Skip to content

Commit

Permalink
feat: add plugin installed event [IDE-736] (#632)
Browse files Browse the repository at this point in the history
* feat: add plugin installed event

* feat: automated-region-configuration (IDE-732) (#631)

* feat: Automated Snyk region configuration (IDE-732)

* tidy: remove legacy functionality for domain/V1

* chore: update changelog

* feat: add plugin installed event

* chore: don't log actual filesystem path

* chore: add changelog, minor visibility change to private methods

* fix: test and visibility

---------

Co-authored-by: Knut Funkel <[email protected]>
  • Loading branch information
bastiandoetsch and acke authored Nov 6, 2024
1 parent 759936f commit 49e2620
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [2.11.0]
### Changed
- If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings.
- Add "plugin installed" analytics event (sent after authentication)
- Added a description of custom endpoints to settings dialog.

## [2.10.0]
Expand Down
13 changes: 0 additions & 13 deletions src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package io.snyk.plugin

import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginInstaller
import com.intellij.ide.plugins.PluginStateListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.extensions.ExtensionPointName
Expand Down Expand Up @@ -36,8 +33,6 @@ class SnykPostStartupActivity : ProjectActivity {

@Suppress("TooGenericExceptionCaught")
override suspend fun execute(project: Project) {
PluginInstaller.addStateListener(UninstallListener())

if (!listenersActivated) {
val messageBusConnection = ApplicationManager.getApplication().messageBus.connect()
// TODO: add subscription for language server messages
Expand Down Expand Up @@ -87,11 +82,3 @@ class SnykPostStartupActivity : ProjectActivity {
}
}
}

private class UninstallListener : PluginStateListener {
@Suppress("EmptyFunctionBlock")
override fun install(descriptor: IdeaPluginDescriptor) {
}

override fun uninstall(descriptor: IdeaPluginDescriptor) {}
}
14 changes: 10 additions & 4 deletions src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package io.snyk.plugin.analytics
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import io.snyk.plugin.events.SnykScanListener
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.toVirtualFile
import snyk.common.SnykError
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.common.lsp.analytics.AnalyticsEvent
import snyk.common.lsp.analytics.ScanDoneEvent
import snyk.container.ContainerResult
import snyk.iac.IacResult

@Service(Service.Level.PROJECT)
class AnalyticsScanListener(val project: Project) {
Expand Down Expand Up @@ -53,7 +53,7 @@ class AnalyticsScanListener(val project: Project) {
containerResult.mediumSeveritiesCount(),
containerResult.lowSeveritiesCount()
)
LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent)
AnalyticsSender.getInstance().logEvent(scanDoneEvent)
}

override fun scanningContainerError(snykError: SnykError) {
Expand All @@ -66,5 +66,11 @@ class AnalyticsScanListener(val project: Project) {
SnykScanListener.SNYK_SCAN_TOPIC,
snykScanListener,
)
if (!pluginSettings().pluginInstalledSent) {
val event = AnalyticsEvent("plugin installed", listOf("install"))
AnalyticsSender.getInstance().logEvent(event) {
pluginSettings().pluginInstalledSent = true
}
}
}
}
63 changes: 63 additions & 0 deletions src/main/kotlin/io/snyk/plugin/analytics/AnalyticsSender.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.snyk.plugin.analytics

import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable
import org.jetbrains.concurrency.runAsync
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.analytics.AbstractAnalyticsEvent
import java.util.LinkedList
import java.util.concurrent.ConcurrentLinkedQueue

class AnalyticsSender : Disposable {
private var disposed: Boolean = false

// left = event, right = callback function
private val eventQueue = ConcurrentLinkedQueue<Pair<AbstractAnalyticsEvent, () -> Unit>>()

init {
Disposer.register(SnykPluginDisposable.getInstance(), this)
start()
}

private fun start() {
runAsync {
val lsw = LanguageServerWrapper.getInstance()
while (!disposed) {
if (eventQueue.isEmpty() || lsw.notAuthenticated()) {
Thread.sleep(1000)
continue
}
val copyForSending = LinkedList(eventQueue)
for (event in copyForSending) {
try {
lsw.sendReportAnalyticsCommand(event.first)
event.second()
} catch (e: Exception) {
lsw.logger.warn("unexpected exception while sending analytics")
} finally {
eventQueue.remove(event)
}
}
}
}
}

fun logEvent(event: AbstractAnalyticsEvent, callback: () -> Unit = {}) = eventQueue.add(Pair(event, callback))

companion object {
private var instance: AnalyticsSender? = null

@JvmStatic
fun getInstance(): AnalyticsSender {
if (instance == null) {
instance = AnalyticsSender()
}
return instance as AnalyticsSender
}
}

override fun dispose() {
this.disposed = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import java.util.UUID
storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)],
)
class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplicationSettingsStateService> {
val requiredLsProtocolVersion = 16
// events
var pluginInstalledSent: Boolean = false

val requiredLsProtocolVersion = 17

var useTokenAuthentication = false
var currentLSProtocolVersion: Int? = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import java.time.LocalDate
import java.time.temporal.ChronoUnit
import java.util.Date

@Suppress("MemberVisibilityCanBePrivate")
@Service
class SnykCliDownloaderService {

Expand Down
13 changes: 6 additions & 7 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import org.eclipse.lsp4j.services.LanguageServer
import org.jetbrains.concurrency.runAsync
import snyk.common.EnvironmentHelper
import snyk.common.getEndpointUrl
import snyk.common.lsp.analytics.AbstractAnalyticsEvent
import snyk.common.lsp.commands.COMMAND_CODE_FIX_DIFFS
import snyk.common.lsp.commands.COMMAND_CODE_SUBMIT_FIX_FEEDBACK
import snyk.common.lsp.commands.COMMAND_COPY_AUTH_LINK
Expand All @@ -59,7 +60,6 @@ import snyk.common.lsp.commands.COMMAND_LOGOUT
import snyk.common.lsp.commands.COMMAND_REPORT_ANALYTICS
import snyk.common.lsp.commands.COMMAND_WORKSPACE_FOLDER_SCAN
import snyk.common.lsp.commands.SNYK_GENERATE_ISSUE_DESCRIPTION
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.common.lsp.progress.ProgressManager
import snyk.common.lsp.settings.LanguageServerSettings
import snyk.common.lsp.settings.SeverityFilter
Expand Down Expand Up @@ -348,10 +348,10 @@ class LanguageServerWrapper(
return isInitialized
}

fun sendReportAnalyticsCommand(scanDoneEvent: ScanDoneEvent) {
fun sendReportAnalyticsCommand(event: AbstractAnalyticsEvent) {
if (notAuthenticated()) return
try {
val eventString = gson.toJson(scanDoneEvent)
val eventString = gson.toJson(event)
val param = ExecuteCommandParams()
param.command = COMMAND_REPORT_ANALYTICS
param.arguments = listOf(eventString)
Expand Down Expand Up @@ -488,8 +488,7 @@ class LanguageServerWrapper(
}

fun getAuthenticatedUser(): String? {
if (pluginSettings().token.isNullOrBlank()) return null
if (!ensureLanguageServerInitialized()) return null
if (notAuthenticated()) return null

if (!this.authenticatedUser.isNullOrEmpty()) return authenticatedUser!!["username"]
val cmd = ExecuteCommandParams(COMMAND_GET_ACTIVE_USER, emptyList())
Expand Down Expand Up @@ -541,7 +540,7 @@ class LanguageServerWrapper(
}

fun generateIssueDescription(issue: ScanIssue): String? {
if (!ensureLanguageServerInitialized()) return null
if (notAuthenticated()) return null
val key = issue.additionalData.key
if (key.isBlank()) throw RuntimeException("Issue ID is required")
val generateIssueCommand = ExecuteCommandParams(SNYK_GENERATE_ISSUE_DESCRIPTION, listOf(key))
Expand Down Expand Up @@ -624,7 +623,7 @@ class LanguageServerWrapper(
}
}

private fun notAuthenticated() = !ensureLanguageServerInitialized() || pluginSettings().token.isNullOrBlank()
fun notAuthenticated() = !ensureLanguageServerInitialized() || pluginSettings().token.isNullOrBlank()


private fun ensureLanguageServerProtocolVersion(project: Project) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package snyk.common.lsp.analytics

interface AbstractAnalyticsEvent
13 changes: 13 additions & 0 deletions src/main/kotlin/snyk/common/lsp/analytics/AnalyticsEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package snyk.common.lsp.analytics

data class AnalyticsEvent(
val interactionType: String,
val category: List<String>,
val status: String = "success",
val targetId: String = "pkg:filesystem/scrubbed",
val timestampMs: Long = System.currentTimeMillis(),
val durationMs: Long = 0,
val results: Map<String, Any> = emptyMap(),
val errors: List<Any> = emptyList(),
val extension: Map<String, Any> = emptyMap(),
) : AbstractAnalyticsEvent
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package snyk.common.lsp.commands
package snyk.common.lsp.analytics

import com.google.gson.annotations.SerializedName
import io.snyk.plugin.getArch
Expand All @@ -9,7 +9,7 @@ import java.time.ZonedDateTime

data class ScanDoneEvent(
@SerializedName("data") val data: Data
) {
) : AbstractAnalyticsEvent {
data class Data(
@SerializedName("type") val type: String = "analytics",
@SerializedName("attributes") val attributes: Attributes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.snyk.plugin.analytics

import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import io.mockk.every
Expand All @@ -14,6 +16,7 @@ import io.snyk.plugin.getOS
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.services.SnykApplicationSettingsStateService
import io.snyk.plugin.toVirtualFile
import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import org.junit.After
Expand All @@ -29,16 +32,22 @@ class AnalyticsScanListenerTest {
private val projectMock: Project = mockk()
private val settings = SnykApplicationSettingsStateService()
private val languageServerWrapper: LanguageServerWrapper = mockk()
private val applicationMock: Application = mockk(relaxed = true)

@Before
fun setUp() {
unmockkAll()

mockkStatic(ApplicationManager::class)
every { ApplicationManager.getApplication() } returns applicationMock
every { applicationMock.getService(SnykPluginDisposable::class.java) } returns mockk(relaxed = true)

mockkStatic("io.snyk.plugin.UtilsKt")
every { pluginSettings() } returns settings

mockkObject(LanguageServerWrapper.Companion)
every { LanguageServerWrapper.getInstance() } returns languageServerWrapper
every { languageServerWrapper.notAuthenticated() } returns false
justRun { languageServerWrapper.sendReportAnalyticsCommand(any()) }

mockkStatic("snyk.PluginInformationKt")
Expand Down Expand Up @@ -104,6 +113,6 @@ class AnalyticsScanListenerTest {
fun `testScanListener scanningContainerFinished should call language server to report analytics`() {
cut.snykScanListener.scanningContainerFinished(mockk(relaxed = true))

verify { languageServerWrapper.sendReportAnalyticsCommand(any()) }
verify(timeout = 3000) { languageServerWrapper.sendReportAnalyticsCommand(any()) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import org.eclipse.lsp4j.services.LanguageServer
import org.junit.After
import org.junit.Before
import org.junit.Test
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.common.lsp.analytics.ScanDoneEvent
import snyk.common.lsp.settings.FolderConfigSettings
import snyk.pluginInfo
import snyk.trust.WorkspaceTrustService
Expand Down

0 comments on commit 49e2620

Please sign in to comment.