Skip to content

Commit 892937d

Browse files
authored
Merge pull request #2483 from digma-ai/feature/posthog-engagment-score
posthog engagment score Closes #2472
2 parents 489c144 + c3e946c commit 892937d

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.digma.intellij.plugin.engagement
2+
3+
import com.intellij.openapi.components.PersistentStateComponent
4+
import com.intellij.openapi.components.Service
5+
import com.intellij.openapi.components.State
6+
import com.intellij.openapi.components.Storage
7+
import com.intellij.util.xmlb.Converter
8+
import com.intellij.util.xmlb.annotations.OptionTag
9+
import com.intellij.util.xmlb.annotations.XMap
10+
import kotlinx.datetime.LocalDate
11+
12+
13+
@State(
14+
name = "org.digma.intellij.plugin.engagement.EngagementScorePersistence",
15+
storages = [Storage("DigmaEngagementScorePersistence.xml")]
16+
)
17+
@Service(Service.Level.APP)
18+
class EngagementScorePersistence : PersistentStateComponent<EngagementScoreData> {
19+
20+
private var myPersistenceData = EngagementScoreData()
21+
22+
override fun getState(): EngagementScoreData {
23+
return myPersistenceData
24+
}
25+
26+
override fun loadState(state: EngagementScoreData) {
27+
myPersistenceData = state
28+
}
29+
}
30+
31+
class EngagementScoreData {
32+
@OptionTag(converter = LocalDateConverter::class)
33+
var lastEventTime: LocalDate? = null
34+
35+
//using string as day and not LocalDate because it's a bit messy to serialise LocalDate as map keys
36+
// with this persistence framework.
37+
@get:XMap(keyAttributeName = "day", valueAttributeName = "count")
38+
var meaningfulActionsCounters = mutableMapOf<String, Int>()
39+
40+
41+
fun put(date: LocalDate, count: Int) {
42+
meaningfulActionsCounters[date.toString()] = count
43+
}
44+
45+
fun get(date: LocalDate): Int? {
46+
return meaningfulActionsCounters[date.toString()]
47+
}
48+
49+
fun increment(date: LocalDate) {
50+
val count = meaningfulActionsCounters[date.toString()]
51+
meaningfulActionsCounters[date.toString()] = count.increment()
52+
}
53+
54+
fun remove(date: LocalDate) {
55+
meaningfulActionsCounters.remove(date.toString())
56+
}
57+
58+
fun remove(date: String) {
59+
meaningfulActionsCounters.remove(date)
60+
}
61+
}
62+
63+
private fun Int?.increment(): Int = if (this == null) 1 else this + 1
64+
65+
66+
private class LocalDateConverter : Converter<LocalDate>() {
67+
override fun fromString(value: String): LocalDate {
68+
return LocalDate.parse(value)
69+
}
70+
71+
override fun toString(value: LocalDate): String {
72+
return value.toString()
73+
}
74+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.digma.intellij.plugin.engagement
2+
3+
import com.intellij.openapi.components.Service
4+
import com.intellij.openapi.components.service
5+
import kotlinx.coroutines.CoroutineScope
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.isActive
8+
import kotlinx.coroutines.launch
9+
import kotlinx.datetime.Clock
10+
import kotlinx.datetime.DatePeriod
11+
import kotlinx.datetime.LocalDate
12+
import kotlinx.datetime.TimeZone
13+
import kotlinx.datetime.plus
14+
import kotlinx.datetime.todayIn
15+
import org.digma.intellij.plugin.common.DisposableAdaptor
16+
import org.digma.intellij.plugin.common.findActiveProject
17+
import org.digma.intellij.plugin.errorreporting.ErrorReporter
18+
import org.digma.intellij.plugin.posthog.ActivityMonitor
19+
import org.digma.intellij.plugin.scheduling.disposingPeriodicTask
20+
import kotlin.math.roundToLong
21+
import kotlin.time.Duration.Companion.hours
22+
import kotlin.time.Duration.Companion.minutes
23+
24+
25+
@Service(Service.Level.APP)
26+
class EngagementScoreService(private val cs: CoroutineScope) : DisposableAdaptor {
27+
28+
companion object {
29+
val MEANINGFUL_ACTIONS = setOf(
30+
"span link clicked",
31+
"insights insight card asset link clicked",
32+
"insights issue card title asset link clicked",
33+
"highlights top issues card asset link clicked",
34+
"insights issue card clicked",
35+
"highlights top issues card table row clicked",
36+
"trace button clicked",
37+
"errors trace button clicked",
38+
"errors error stack trace item clicked",
39+
"insights jira ticket info button clicked",
40+
"open histogram",
41+
"issues filter changed",
42+
)
43+
44+
private val PERIOD_TO_TRACK = DatePeriod.parse("P21D")
45+
private val TIME_ZONE = TimeZone.currentSystemDefault()
46+
47+
fun today(): LocalDate {
48+
return Clock.System.todayIn(TIME_ZONE)
49+
}
50+
}
51+
52+
53+
init {
54+
55+
disposingPeriodicTask("DailyEngagementScore", 1.minutes.inWholeMilliseconds, 6.hours.inWholeMilliseconds, false) {
56+
try {
57+
removeOldEntries()
58+
if (isDailyEventTime()) {
59+
sendEvent()
60+
}
61+
} catch (e: Throwable) {
62+
ErrorReporter.getInstance().reportError("EngagementScoreManager.DailyEngagementScore", e)
63+
}
64+
}
65+
}
66+
67+
private fun sendEvent() {
68+
69+
//compute average that includes up to the last day, exclude today.
70+
val daysForAverage = service<EngagementScorePersistence>().state.meaningfulActionsCounters.filter {
71+
LocalDate.parse(it.key) != today()
72+
}
73+
74+
if (daysForAverage.isEmpty()){
75+
return
76+
}
77+
78+
79+
val activeDays = daysForAverage.size
80+
val average = daysForAverage.values.average().let {
81+
if (it.isNaN()) 0 else it.roundToLong()
82+
}
83+
84+
//todo: when ActivityMonitor is changed to application service this coroutine can be removed and just send the event
85+
//this is just a technical debt that we have:
86+
//ActivityMonitor is a project service, EngagementScoreService is an application service
87+
// and doesn't have a reference to a project, usually we use findActiveProject() to get a reference to
88+
// a project and get the ActivityMonitor service.
89+
//but this event is daily, if we don't find an active project we'll miss the event time.
90+
//an active project will not be found if user closed the last project exactly at this moment.
91+
//so we wait at least 30 minutes to find an active project, if we can't find give up and hopefully the next
92+
//event will be sent
93+
cs.launch {
94+
95+
val startTime = Clock.System.now()
96+
97+
var project = findActiveProject()
98+
while (isActive && project == null && startTime.plus(30.minutes) > Clock.System.now()) {
99+
delay(1.minutes.inWholeMilliseconds)
100+
project = findActiveProject()
101+
}
102+
103+
project?.let {
104+
105+
//update last event time only if really sent the event
106+
service<EngagementScorePersistence>().state.lastEventTime = today()
107+
108+
ActivityMonitor.getInstance(it).registerCustomEvent(
109+
"daily engagement score", mapOf(
110+
"meaningful_actions_days" to activeDays,
111+
"meaningful_actions_avg" to average
112+
)
113+
)
114+
}
115+
}
116+
}
117+
118+
private fun isDailyEventTime(): Boolean {
119+
return service<EngagementScorePersistence>().state.lastEventTime?.let {
120+
today() > it
121+
} ?: true
122+
}
123+
124+
125+
private fun removeOldEntries() {
126+
val oldEntries = service<EngagementScorePersistence>().state.meaningfulActionsCounters.keys.filter {
127+
LocalDate.parse(it).plus(PERIOD_TO_TRACK) < today()
128+
}
129+
130+
oldEntries.forEach {
131+
service<EngagementScorePersistence>().state.remove(it)
132+
}
133+
}
134+
135+
136+
fun addAction(action: String) {
137+
if (MEANINGFUL_ACTIONS.contains(action)) {
138+
service<EngagementScorePersistence>().state.increment(today())
139+
}
140+
}
141+
142+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.digma.intellij.plugin.engagement
2+
3+
import com.intellij.openapi.components.service
4+
import com.intellij.openapi.project.Project
5+
import com.intellij.openapi.startup.ProjectActivity
6+
7+
class EngagementScoreServiceStartup : ProjectActivity {
8+
override suspend fun execute(project: Project) {
9+
service<EngagementScoreService>()
10+
}
11+
}

ide-common/src/main/kotlin/org/digma/intellij/plugin/posthog/ActivityMonitor.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException
44
import com.intellij.openapi.Disposable
55
import com.intellij.openapi.application.ApplicationInfo
66
import com.intellij.openapi.components.Service
7+
import com.intellij.openapi.components.service
78
import com.intellij.openapi.diagnostic.Logger
89
import com.intellij.openapi.progress.ProcessCanceledException
910
import com.intellij.openapi.project.Project
@@ -20,6 +21,7 @@ import org.digma.intellij.plugin.common.ExceptionUtils
2021
import org.digma.intellij.plugin.common.Frequency
2122
import org.digma.intellij.plugin.common.UniqueGeneratedUserId
2223
import org.digma.intellij.plugin.common.objectToJson
24+
import org.digma.intellij.plugin.engagement.EngagementScoreService
2325
import org.digma.intellij.plugin.execution.DIGMA_INSTRUMENTATION_ERROR
2426
import org.digma.intellij.plugin.log.Log
2527
import org.digma.intellij.plugin.model.rest.AboutResult
@@ -252,6 +254,8 @@ class ActivityMonitor(private val project: Project, cs: CoroutineScope) : Dispos
252254
val detailsToSend = details.toMutableMap()
253255
detailsToSend["action"] = action
254256

257+
service<EngagementScoreService>().addAction(action)
258+
255259
capture(
256260
"user-action",
257261
detailsToSend

src/main/resources/META-INF/plugin.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@
119119
implementation="org.digma.intellij.plugin.posthog.PerformanceMetricsPosthogEventStartupActivity"/>
120120
<postStartupActivity
121121
implementation="org.digma.intellij.plugin.activation.UserActivationStartup"/>
122+
<postStartupActivity
123+
implementation="org.digma.intellij.plugin.engagement.EngagementScoreServiceStartup"/>
122124

123125

124126

0 commit comments

Comments
 (0)