Skip to content

Commit 49e2620

Browse files
bastiandoetschacke
andauthored
feat: add plugin installed event [IDE-736] (#632)
* 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]>
1 parent 759936f commit 49e2620

File tree

12 files changed

+114
-29
lines changed

12 files changed

+114
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [2.11.0]
44
### Changed
55
- If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings.
6+
- Add "plugin installed" analytics event (sent after authentication)
67
- Added a description of custom endpoints to settings dialog.
78

89
## [2.10.0]

src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package io.snyk.plugin
22

3-
import com.intellij.ide.plugins.IdeaPluginDescriptor
4-
import com.intellij.ide.plugins.PluginInstaller
5-
import com.intellij.ide.plugins.PluginStateListener
63
import com.intellij.openapi.application.ApplicationManager
74
import com.intellij.openapi.diagnostic.Logger
85
import com.intellij.openapi.extensions.ExtensionPointName
@@ -36,8 +33,6 @@ class SnykPostStartupActivity : ProjectActivity {
3633

3734
@Suppress("TooGenericExceptionCaught")
3835
override suspend fun execute(project: Project) {
39-
PluginInstaller.addStateListener(UninstallListener())
40-
4136
if (!listenersActivated) {
4237
val messageBusConnection = ApplicationManager.getApplication().messageBus.connect()
4338
// TODO: add subscription for language server messages
@@ -87,11 +82,3 @@ class SnykPostStartupActivity : ProjectActivity {
8782
}
8883
}
8984
}
90-
91-
private class UninstallListener : PluginStateListener {
92-
@Suppress("EmptyFunctionBlock")
93-
override fun install(descriptor: IdeaPluginDescriptor) {
94-
}
95-
96-
override fun uninstall(descriptor: IdeaPluginDescriptor) {}
97-
}

src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package io.snyk.plugin.analytics
33
import com.intellij.openapi.components.Service
44
import com.intellij.openapi.project.Project
55
import io.snyk.plugin.events.SnykScanListener
6+
import io.snyk.plugin.pluginSettings
67
import io.snyk.plugin.toVirtualFile
78
import snyk.common.SnykError
8-
import snyk.common.lsp.LanguageServerWrapper
9-
import snyk.common.lsp.commands.ScanDoneEvent
9+
import snyk.common.lsp.analytics.AnalyticsEvent
10+
import snyk.common.lsp.analytics.ScanDoneEvent
1011
import snyk.container.ContainerResult
11-
import snyk.iac.IacResult
1212

1313
@Service(Service.Level.PROJECT)
1414
class AnalyticsScanListener(val project: Project) {
@@ -53,7 +53,7 @@ class AnalyticsScanListener(val project: Project) {
5353
containerResult.mediumSeveritiesCount(),
5454
containerResult.lowSeveritiesCount()
5555
)
56-
LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent)
56+
AnalyticsSender.getInstance().logEvent(scanDoneEvent)
5757
}
5858

5959
override fun scanningContainerError(snykError: SnykError) {
@@ -66,5 +66,11 @@ class AnalyticsScanListener(val project: Project) {
6666
SnykScanListener.SNYK_SCAN_TOPIC,
6767
snykScanListener,
6868
)
69+
if (!pluginSettings().pluginInstalledSent) {
70+
val event = AnalyticsEvent("plugin installed", listOf("install"))
71+
AnalyticsSender.getInstance().logEvent(event) {
72+
pluginSettings().pluginInstalledSent = true
73+
}
74+
}
6975
}
7076
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.snyk.plugin.analytics
2+
3+
import com.intellij.openapi.Disposable
4+
import com.intellij.openapi.util.Disposer
5+
import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable
6+
import org.jetbrains.concurrency.runAsync
7+
import snyk.common.lsp.LanguageServerWrapper
8+
import snyk.common.lsp.analytics.AbstractAnalyticsEvent
9+
import java.util.LinkedList
10+
import java.util.concurrent.ConcurrentLinkedQueue
11+
12+
class AnalyticsSender : Disposable {
13+
private var disposed: Boolean = false
14+
15+
// left = event, right = callback function
16+
private val eventQueue = ConcurrentLinkedQueue<Pair<AbstractAnalyticsEvent, () -> Unit>>()
17+
18+
init {
19+
Disposer.register(SnykPluginDisposable.getInstance(), this)
20+
start()
21+
}
22+
23+
private fun start() {
24+
runAsync {
25+
val lsw = LanguageServerWrapper.getInstance()
26+
while (!disposed) {
27+
if (eventQueue.isEmpty() || lsw.notAuthenticated()) {
28+
Thread.sleep(1000)
29+
continue
30+
}
31+
val copyForSending = LinkedList(eventQueue)
32+
for (event in copyForSending) {
33+
try {
34+
lsw.sendReportAnalyticsCommand(event.first)
35+
event.second()
36+
} catch (e: Exception) {
37+
lsw.logger.warn("unexpected exception while sending analytics")
38+
} finally {
39+
eventQueue.remove(event)
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
fun logEvent(event: AbstractAnalyticsEvent, callback: () -> Unit = {}) = eventQueue.add(Pair(event, callback))
47+
48+
companion object {
49+
private var instance: AnalyticsSender? = null
50+
51+
@JvmStatic
52+
fun getInstance(): AnalyticsSender {
53+
if (instance == null) {
54+
instance = AnalyticsSender()
55+
}
56+
return instance as AnalyticsSender
57+
}
58+
}
59+
60+
override fun dispose() {
61+
this.disposed = true
62+
}
63+
}

src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import java.util.UUID
2626
storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)],
2727
)
2828
class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplicationSettingsStateService> {
29-
val requiredLsProtocolVersion = 16
29+
// events
30+
var pluginInstalledSent: Boolean = false
31+
32+
val requiredLsProtocolVersion = 17
3033

3134
var useTokenAuthentication = false
3235
var currentLSProtocolVersion: Int? = 0

src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import java.time.LocalDate
1616
import java.time.temporal.ChronoUnit
1717
import java.util.Date
1818

19+
@Suppress("MemberVisibilityCanBePrivate")
1920
@Service
2021
class SnykCliDownloaderService {
2122

src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import org.eclipse.lsp4j.services.LanguageServer
4747
import org.jetbrains.concurrency.runAsync
4848
import snyk.common.EnvironmentHelper
4949
import snyk.common.getEndpointUrl
50+
import snyk.common.lsp.analytics.AbstractAnalyticsEvent
5051
import snyk.common.lsp.commands.COMMAND_CODE_FIX_DIFFS
5152
import snyk.common.lsp.commands.COMMAND_CODE_SUBMIT_FIX_FEEDBACK
5253
import snyk.common.lsp.commands.COMMAND_COPY_AUTH_LINK
@@ -59,7 +60,6 @@ import snyk.common.lsp.commands.COMMAND_LOGOUT
5960
import snyk.common.lsp.commands.COMMAND_REPORT_ANALYTICS
6061
import snyk.common.lsp.commands.COMMAND_WORKSPACE_FOLDER_SCAN
6162
import snyk.common.lsp.commands.SNYK_GENERATE_ISSUE_DESCRIPTION
62-
import snyk.common.lsp.commands.ScanDoneEvent
6363
import snyk.common.lsp.progress.ProgressManager
6464
import snyk.common.lsp.settings.LanguageServerSettings
6565
import snyk.common.lsp.settings.SeverityFilter
@@ -348,10 +348,10 @@ class LanguageServerWrapper(
348348
return isInitialized
349349
}
350350

351-
fun sendReportAnalyticsCommand(scanDoneEvent: ScanDoneEvent) {
351+
fun sendReportAnalyticsCommand(event: AbstractAnalyticsEvent) {
352352
if (notAuthenticated()) return
353353
try {
354-
val eventString = gson.toJson(scanDoneEvent)
354+
val eventString = gson.toJson(event)
355355
val param = ExecuteCommandParams()
356356
param.command = COMMAND_REPORT_ANALYTICS
357357
param.arguments = listOf(eventString)
@@ -488,8 +488,7 @@ class LanguageServerWrapper(
488488
}
489489

490490
fun getAuthenticatedUser(): String? {
491-
if (pluginSettings().token.isNullOrBlank()) return null
492-
if (!ensureLanguageServerInitialized()) return null
491+
if (notAuthenticated()) return null
493492

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

543542
fun generateIssueDescription(issue: ScanIssue): String? {
544-
if (!ensureLanguageServerInitialized()) return null
543+
if (notAuthenticated()) return null
545544
val key = issue.additionalData.key
546545
if (key.isBlank()) throw RuntimeException("Issue ID is required")
547546
val generateIssueCommand = ExecuteCommandParams(SNYK_GENERATE_ISSUE_DESCRIPTION, listOf(key))
@@ -624,7 +623,7 @@ class LanguageServerWrapper(
624623
}
625624
}
626625

627-
private fun notAuthenticated() = !ensureLanguageServerInitialized() || pluginSettings().token.isNullOrBlank()
626+
fun notAuthenticated() = !ensureLanguageServerInitialized() || pluginSettings().token.isNullOrBlank()
628627

629628

630629
private fun ensureLanguageServerProtocolVersion(project: Project) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package snyk.common.lsp.analytics
2+
3+
interface AbstractAnalyticsEvent
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package snyk.common.lsp.analytics
2+
3+
data class AnalyticsEvent(
4+
val interactionType: String,
5+
val category: List<String>,
6+
val status: String = "success",
7+
val targetId: String = "pkg:filesystem/scrubbed",
8+
val timestampMs: Long = System.currentTimeMillis(),
9+
val durationMs: Long = 0,
10+
val results: Map<String, Any> = emptyMap(),
11+
val errors: List<Any> = emptyList(),
12+
val extension: Map<String, Any> = emptyMap(),
13+
) : AbstractAnalyticsEvent

src/main/kotlin/snyk/common/lsp/commands/ScanDoneEvent.kt renamed to src/main/kotlin/snyk/common/lsp/analytics/ScanDoneEvent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package snyk.common.lsp.commands
1+
package snyk.common.lsp.analytics
22

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

1010
data class ScanDoneEvent(
1111
@SerializedName("data") val data: Data
12-
) {
12+
) : AbstractAnalyticsEvent {
1313
data class Data(
1414
@SerializedName("type") val type: String = "analytics",
1515
@SerializedName("attributes") val attributes: Attributes

0 commit comments

Comments
 (0)