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+ }
0 commit comments