Skip to content

Commit 527816b

Browse files
authored
Merge pull request #2134 from digma-ai/feature/digmathon-changes
Update Digmathon
2 parents 3300852 + 0f4a147 commit 527816b

File tree

15 files changed

+349
-183
lines changed

15 files changed

+349
-183
lines changed

ide-common/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
api(libs.maven.artifact)
3232
api(libs.glovoapp.versioning)
3333
api(libs.byte.buddy)
34+
api(libs.jackson.datetime)
3435

3536

3637
implementation(project(":model"))

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,39 @@ package org.digma.intellij.plugin.common
33
import com.fasterxml.jackson.databind.ObjectMapper
44
import com.fasterxml.jackson.databind.SerializationFeature
55
import com.fasterxml.jackson.databind.util.StdDateFormat
6+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
67

78

89
/**
910
* a factory for ObjectMapper to be used by components and services that need an ObjectMapper,
1011
* makes sure all will use the same configuration.
1112
*/
1213
fun createObjectMapper(): ObjectMapper {
14+
//Note that it is risky to change configuration or install modules.
15+
// because there are components that already use this object mapper as is
16+
// and changes to configuration may cause issues. its probably mainly dates that were
17+
// serialized to persistence.
18+
//but, probably installing the JavaTimeModule should be OK because it's backwards compatible.
19+
//users of this factory may change configuration on the instance they create.
1320
val objectMapper = ObjectMapper()
1421
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
1522
objectMapper.setDateFormat(StdDateFormat())
1623
return objectMapper
1724
}
1825

26+
27+
fun createObjectMapperWithJavaTimeModule(): ObjectMapper {
28+
//install the JavaTimeModule for better serialization of dates
29+
val objectMapper = ObjectMapper()
30+
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
31+
objectMapper.registerModules(JavaTimeModule())
32+
objectMapper.setDateFormat(StdDateFormat())
33+
return objectMapper
34+
}
35+
36+
37+
38+
1939
/**
2040
* ObjectMapper is fully thread safe and does not need to be created many times.
2141
* usually its more convenient to create a class member in classes that need to use an ObjectMapper

ide-common/src/main/kotlin/org/digma/intellij/plugin/digmathon/DigmathonProductKey.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import java.nio.charset.StandardCharsets
99

1010

1111
private const val MY_SERVICE = "org.digma.digmathon.productKey"
12-
private const val MY_KEY = "product-key-2024.4"
12+
13+
//change MY_KEY for every new dismathon
14+
private const val MY_KEY = "product-key-2024.5"
1315

1416
class DigmathonProductKey {
1517

16-
private val myHash = "b9fe040958b98f68533511125bc104435bfefd4293b328060992767d51333321"
18+
private val myHash = "89e0f23f9a0a670b2bb7393a1280aa59eaf7c59c22b77d869b5c9c7af021785f"
1719

1820
@Throws(InvalidProductKeyException::class)
1921
fun validateAndSave(productKey: String) {

ide-common/src/main/kotlin/org/digma/intellij/plugin/digmathon/DigmathonService.kt

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.digma.intellij.plugin.digmathon
22

3+
import com.fasterxml.jackson.core.type.TypeReference
34
import com.intellij.collaboration.async.disposingScope
45
import com.intellij.openapi.Disposable
56
import com.intellij.openapi.application.ApplicationManager
@@ -12,17 +13,18 @@ import kotlinx.coroutines.delay
1213
import kotlinx.coroutines.isActive
1314
import kotlinx.coroutines.launch
1415
import org.digma.intellij.plugin.common.allowSlowOperation
16+
import org.digma.intellij.plugin.common.createObjectMapperWithJavaTimeModule
1517
import org.digma.intellij.plugin.common.findActiveProject
1618
import org.digma.intellij.plugin.errorreporting.ErrorReporter
1719
import org.digma.intellij.plugin.log.Log
1820
import org.digma.intellij.plugin.notifications.NotificationUtil
1921
import org.digma.intellij.plugin.persistence.PersistenceService
2022
import org.digma.intellij.plugin.posthog.ActivityMonitor
23+
import java.time.Instant
2124
import java.time.LocalDate
2225
import java.time.LocalDateTime
2326
import java.time.ZoneId
2427
import java.time.ZonedDateTime
25-
import java.util.concurrent.atomic.AtomicBoolean
2628
import java.util.concurrent.atomic.AtomicReference
2729
import java.util.function.Supplier
2830
import kotlin.time.Duration.Companion.minutes
@@ -31,25 +33,24 @@ import kotlin.time.Duration.Companion.minutes
3133
@Service(Service.Level.APP)
3234
class DigmathonService : Disposable {
3335

34-
3536
private val logger = Logger.getInstance(this::class.java)
3637

3738
private val digmathonInfo = AtomicReference(
3839
DigmathonInfo(
39-
LocalDate.of(2024, 4, 5).atStartOfDay().atZone(ZoneId.systemDefault()),
40-
LocalDate.of(2024, 4, 17).atStartOfDay().atZone(ZoneId.systemDefault())
40+
LocalDate.of(2024, 5, 1).atStartOfDay().atZone(ZoneId.systemDefault()),
41+
LocalDate.of(2024, 5, 14).atStartOfDay().atZone(ZoneId.systemDefault())
4142
)
4243
)
4344

44-
private val isDigmathonActive = AtomicBoolean(digmathonInfo.get().isActive())
45-
46-
val viewedInsights = PersistenceService.getInstance().getDigmathonInsightsViewed()
47-
?.split(",")?.toMutableSet() ?: mutableSetOf()
45+
val viewedInsights: MutableMap<String, Instant> = readInsightsViewedFromPersistence()
4846

4947
var isUserFinishedDigmathon = PersistenceService.getInstance().isFinishDigmathonGameForUser()
5048

5149

5250
companion object {
51+
52+
private val objectMapper = createObjectMapperWithJavaTimeModule()
53+
5354
@JvmStatic
5455
fun getInstance(): DigmathonService {
5556
return service<DigmathonService>()
@@ -64,27 +65,7 @@ class DigmathonService : Disposable {
6465

6566
init {
6667

67-
//for development,simulate a 5 minutes event that starts 2 minutes after IDE start and lasts for 5 minutes
68-
// val simulateStart = System.getProperty("org.digma.digmathon.simulate.startAfterMinutes")
69-
// val simulatePeriod = System.getProperty("org.digma.digmathon.simulate.periodMinutes")
70-
//
71-
// if (simulateStart != null) {
72-
// val startAfter = simulateStart.toLong()
73-
// val endAfter = (simulatePeriod.toLongOrNull() ?: 10) + startAfter
74-
//
75-
// digmathonInfo.set(
76-
// DigmathonInfo(
77-
// LocalDateTime.now().plusMinutes(startAfter).atZone(ZoneId.systemDefault()),
78-
// LocalDateTime.now().plusMinutes(endAfter).atZone(ZoneId.systemDefault())
79-
// )
80-
// )
81-
// DigmathonProductKey().clear()
82-
// isDigmathonActive.set(digmathonInfo.get().isActive())
83-
// isUserFinishedDigmathon = false
84-
// }
85-
86-
87-
if (isDigmathonActive.get() && digmathonInfo.get().isEnded()) {
68+
if (isDigmathonStartedForUser() && digmathonInfo.get().isEnded()) {
8869
end()
8970
} else {
9071

@@ -97,11 +78,11 @@ class DigmathonService : Disposable {
9778
while (isActive && digmathonInfo.get().isActive()) {
9879
try {
9980

100-
if (!isDigmathonActive.get() && digmathonInfo.get().isActive()) {
81+
if (!isDigmathonStartedForUser() && digmathonInfo.get().isActive()) {
10182
start()
10283
}
10384

104-
if (isDigmathonActive.get() && digmathonInfo.get().isEnded()) {
85+
if (isDigmathonStartedForUser() && digmathonInfo.get().isEnded()) {
10586
end()
10687
cancel("digmathon ended")
10788
}
@@ -116,23 +97,46 @@ class DigmathonService : Disposable {
11697
}
11798
}
11899

119-
if (digmathonInfo.get().isEnded()) {
100+
//the job may be canceled by the system, do a last check
101+
if (isDigmathonStartedForUser() && digmathonInfo.get().isEnded()) {
120102
end()
121103
}
122104
}
123105
}
124106
}
125107

126108

109+
//this is needed so that we can call start and end only once
110+
private fun isDigmathonStartedForUser(): Boolean {
111+
return PersistenceService.getInstance().isDigmathonStartedForUser()
112+
}
113+
114+
private fun markStartedDigmathonForUser() {
115+
PersistenceService.getInstance().setDigmathonStartedForUser(true)
116+
}
117+
118+
private fun markEndedDigmathonForUser() {
119+
PersistenceService.getInstance().setDigmathonStartedForUser(false)
120+
}
121+
122+
127123
private fun start() {
128-
isDigmathonActive.set(true)
124+
//reset persistence properties every time a new digmathon starts
125+
PersistenceService.getInstance().setFinishDigmathonGameForUser(false)
126+
PersistenceService.getInstance().setDigmathonInsightsViewed(null)
127+
PersistenceService.getInstance().setDigmathonInsightsViewedLastUpdated(null)
128+
markStartedDigmathonForUser()
129129
fireStateChangedEvent()
130130
reportEvent("start")
131131
}
132132

133133

134134
private fun end() {
135-
isDigmathonActive.set(false)
135+
//reset persistence properties for next time
136+
PersistenceService.getInstance().setFinishDigmathonGameForUser(false)
137+
PersistenceService.getInstance().setDigmathonInsightsViewed(null)
138+
PersistenceService.getInstance().setDigmathonInsightsViewedLastUpdated(null)
139+
markEndedDigmathonForUser()
136140
DigmathonProductKey().clear()
137141
fireStateChangedEvent()
138142
fireProductKeyStateChanged()
@@ -201,43 +205,62 @@ class DigmathonService : Disposable {
201205
}
202206

203207

204-
data class DigmathonInfo(val startTime: ZonedDateTime, val endTime: ZonedDateTime) {
205-
206-
fun isActive(): Boolean {
207-
val now = LocalDateTime.now().atZone(ZoneId.systemDefault())
208-
209-
return (now.equals(startTime) || now.isAfter(startTime)) &&
210-
now.isBefore(endTime)
211-
}
212-
213-
fun isEnded(): Boolean {
214-
return !isActive()
215-
}
216-
}
217-
218-
219208
private fun reportEvent(eventType: String, details: Map<String, String> = mapOf()) {
220209
findActiveProject()?.let {
221210
ActivityMonitor.getInstance(it).reportDigmathonEvent(eventType, details)
222211
}
223212
}
224213

225214
fun addInsightsViewed(insightsTypesViewed: List<String>) {
226-
if (digmathonInfo.get().isActive()) {
227-
this.viewedInsights.addAll(insightsTypesViewed)
215+
if (digmathonInfo.get().isActive() && !allInsightsExists(insightsTypesViewed)) {
216+
//add only new insight types
217+
insightsTypesViewed.forEach {
218+
this.viewedInsights.computeIfAbsent(it) { Instant.now() }
219+
}
228220
flushInsightsViewedToPersistence()
229221
}
230222
}
231223

224+
private fun allInsightsExists(insightsTypesViewed: List<String>): Boolean {
225+
return this.viewedInsights.keys.containsAll(insightsTypesViewed)
226+
}
227+
228+
232229
private fun flushInsightsViewedToPersistence() {
233-
PersistenceService.getInstance().setDigmathonInsightsViewed(viewedInsights.joinToString(","))
230+
try {
231+
val json = objectMapper.writeValueAsString(viewedInsights)
232+
PersistenceService.getInstance().setDigmathonInsightsViewed(json)
233+
} catch (e: Throwable) {
234+
ErrorReporter.getInstance().reportError("DigmathonService.flushInsightsViewedToPersistence", e)
235+
}
236+
}
237+
238+
private fun readInsightsViewedFromPersistence(): MutableMap<String, Instant> {
239+
val json = PersistenceService.getInstance().getDigmathonInsightsViewed() ?: return mutableMapOf()
240+
return try {
241+
val ref = object : TypeReference<Map<String, Instant>>() {}
242+
val viewedInsights = objectMapper.readValue(json, ref)
243+
viewedInsights.toMutableMap()
244+
} catch (e: Throwable) {
245+
ErrorReporter.getInstance().reportError("DigmathonService.readInsightsViewedFromPersistence", e)
246+
mutableMapOf()
247+
}
248+
}
249+
250+
251+
fun getDigmathonInsightsViewedLastUpdated(): Instant? {
252+
return PersistenceService.getInstance().getDigmathonInsightsViewedLastUpdated()
253+
}
254+
255+
fun updateDigmathonInsightsViewedLastUpdated() {
256+
PersistenceService.getInstance().setDigmathonInsightsViewedLastUpdated(Instant.now())
234257
}
235258

236259
fun setFinishDigmathonGameForUser() {
237-
PersistenceService.getInstance().setFinishDigmathonGameForUser()
260+
PersistenceService.getInstance().setFinishDigmathonGameForUser(true)
238261
isUserFinishedDigmathon = true
239262
fireUserFinishedDigmathon()
240-
reportEvent("user finished game")
263+
reportEvent("user finished digmathon")
241264
}
242265

243266

@@ -246,4 +269,20 @@ class DigmathonService : Disposable {
246269
.userFinishedDigmathon()
247270
}
248271

272+
273+
data class DigmathonInfo(val startTime: ZonedDateTime, val endTime: ZonedDateTime) {
274+
275+
fun isActive(): Boolean {
276+
val now = LocalDateTime.now().atZone(ZoneId.systemDefault())
277+
278+
return (now.equals(startTime) || now.isAfter(startTime)) &&
279+
now.isBefore(endTime)
280+
}
281+
282+
fun isEnded(): Boolean {
283+
return !isActive()
284+
}
285+
}
286+
287+
249288
}

ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceData.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ internal data class PersistenceData(
9191

9292
var isFinishDigmathonGameForUser: Boolean = false,
9393
var digmathonViewedInsights: String? = null,
94+
@OptionTag(converter = InstantConverter::class)
95+
var digmathonViewedInsightsLastUpdated: Instant? = null,
96+
var digmathonStartedForUser: Boolean = false
9497

95-
96-
)
98+
)

ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceService.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,20 +300,38 @@ class PersistenceService {
300300
return state.loadWarningAppearedTimestamp != null
301301
}
302302

303-
fun setFinishDigmathonGameForUser() {
304-
state.isFinishDigmathonGameForUser = true
303+
fun setFinishDigmathonGameForUser(isFinished: Boolean) {
304+
state.isFinishDigmathonGameForUser = isFinished
305305
}
306306

307307
fun isFinishDigmathonGameForUser(): Boolean {
308308
return state.isFinishDigmathonGameForUser
309309
}
310310

311-
fun setDigmathonInsightsViewed(insights: String) {
311+
//nullable so it can be reset every time a new digmathon starts
312+
fun setDigmathonInsightsViewed(insights: String?) {
312313
state.digmathonViewedInsights = insights
313314
}
314315

315316
fun getDigmathonInsightsViewed(): String? {
316317
return state.digmathonViewedInsights
317318
}
318319

320+
fun setDigmathonInsightsViewedLastUpdated(instant: Instant?) {
321+
state.digmathonViewedInsightsLastUpdated = instant
322+
}
323+
324+
fun getDigmathonInsightsViewedLastUpdated(): Instant? {
325+
return state.digmathonViewedInsightsLastUpdated
326+
}
327+
328+
fun isDigmathonStartedForUser(): Boolean {
329+
return state.digmathonStartedForUser
330+
}
331+
332+
fun setDigmathonStartedForUser(started: Boolean) {
333+
state.digmathonStartedForUser = started
334+
}
335+
336+
319337
}

src/main/kotlin/org/digma/intellij/plugin/ui/insights/InsightsMessageRouterHandler.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ class InsightsMessageRouterHandler(project: Project) : AbstractInsightsMessageRo
8787
DigmathonService.getInstance().addInsightsViewed(insightTypeList.map { it.first })
8888
//stop sending this message if user finished the digmathon
8989
if (!DigmathonService.getInstance().isUserFinishedDigmathon) {
90-
RecentActivityService.getInstance(project).setDigmathonProgressData(DigmathonService.getInstance().viewedInsights)
90+
RecentActivityService.getInstance(project)
91+
.setDigmathonProgressData(
92+
DigmathonService.getInstance().viewedInsights,
93+
DigmathonService.getInstance().getDigmathonInsightsViewedLastUpdated()
94+
)
9195
}
9296
}
9397
}

0 commit comments

Comments
 (0)