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+
70+ val activeDays = service<EngagementScorePersistence >().state.meaningfulActionsCounters.size
71+ val average = service<EngagementScorePersistence >().state.meaningfulActionsCounters.values.average().roundToLong()
72+
73+ // todo: when ActivityMonitor is changed to application service this coroutine can be removed and just send the event
74+ // this is just a technical debt that we have:
75+ // ActivityMonitor is a project service, EngagementScoreService is an application service
76+ // and doesn't have a reference to a project, usually we use findActiveProject() to get a reference to
77+ // a project and get the ActivityMonitor service.
78+ // but this event is daily, if we don't find an active project we'll miss the event time.
79+ // an active project will not be found if user closed the last project exactly at this moment.
80+ // so we wait at least 30 minutes to find an active project, if we can't find give up and hopefully the next
81+ // event will be sent
82+ cs.launch {
83+
84+ val startTime = Clock .System .now()
85+
86+ var project = findActiveProject()
87+ while (isActive && project == null && startTime.plus(30 .minutes) > Clock .System .now()) {
88+ delay(1 .minutes.inWholeMilliseconds)
89+ project = findActiveProject()
90+ }
91+
92+ project?.let {
93+
94+ // update last event time only if really send the event
95+ service<EngagementScorePersistence >().state.lastEventTime = today()
96+
97+ ActivityMonitor .getInstance(it).registerCustomEvent(
98+ " daily engagement score" , mapOf (
99+ " meaningful_actions_days" to activeDays,
100+ " meaningful_actions_avg" to average
101+ )
102+ )
103+ }
104+ }
105+ }
106+
107+ private fun isDailyEventTime (): Boolean {
108+ return service<EngagementScorePersistence >().state.lastEventTime?.let {
109+ today() > it
110+ } ? : true
111+ }
112+
113+
114+ private fun removeOldEntries () {
115+ val oldEntries = service<EngagementScorePersistence >().state.meaningfulActionsCounters.keys.filter {
116+ LocalDate .parse(it).plus(PERIOD_TO_TRACK ) < today()
117+ }
118+
119+ oldEntries.forEach {
120+ service<EngagementScorePersistence >().state.remove(it)
121+ }
122+ }
123+
124+
125+ fun addAction (action : String ) {
126+ if (MEANINGFUL_ACTIONS .contains(action)) {
127+ service<EngagementScorePersistence >().state.increment(today())
128+ }
129+ }
130+
131+ }
0 commit comments