Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ apply from: "gradle/spring.gradle"
apply from: "gradle/logging.gradle"
apply from: "gradle/monitor.gradle"
apply from: "gradle/jetbrains.gradle"
apply from: "gradle/analytics.gradle"

sentry {
includeSourceContext = true
Expand Down
3 changes: 3 additions & 0 deletions gradle/analytics.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation "com.mixpanel:mixpanel-java:1.5.4"
}
9 changes: 9 additions & 0 deletions src/main/kotlin/org/gitanimals/core/event/EventLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.gitanimals.core.event

interface EventLogger {

/**
* 분석 이벤트를 트래킹한다. 절대 예외를 throw 하지 않으며, 실패는 구현체 내부에서 로깅으로 처리된다.
*/
fun track(eventName: String, distinctId: String, properties: Map<String, Any?> = emptyMap())
}
31 changes: 31 additions & 0 deletions src/main/kotlin/org/gitanimals/core/event/MixpanelConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.gitanimals.core.event

import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile

@Configuration
class MixpanelConfiguration(
@Value("\${mixpanel.project.token:}") private val token: String,
) {

private val logger = LoggerFactory.getLogger(this::class.java)

@Bean
@Profile("!test")
fun mixpanelEventLogger(): EventLogger {
if (token.isBlank()) {
logger.warn("MIXPANEL_PROJECT_TOKEN is blank — falling back to NoOpEventLogger")
return NoOpEventLogger()
}
return MixpanelEventLogger(MixpanelAPI(), MessageBuilder(token))
}

@Bean
@Profile("test")
fun noOpEventLogger(): EventLogger = NoOpEventLogger()
}
31 changes: 31 additions & 0 deletions src/main/kotlin/org/gitanimals/core/event/MixpanelEventLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.gitanimals.core.event

import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import org.gitanimals.core.GracefulShutdownDispatcher.gracefulLaunch
import org.json.JSONObject
import org.slf4j.LoggerFactory

class MixpanelEventLogger(
private val mixpanelAPI: MixpanelAPI,
private val messageBuilder: MessageBuilder,
) : EventLogger {

private val logger = LoggerFactory.getLogger(this::class.java)

override fun track(eventName: String, distinctId: String, properties: Map<String, Any?>) {
gracefulLaunch {
runCatching {
val props = JSONObject()
properties.forEach { (k, v) -> if (v != null) props.put(k, v) }
val message = messageBuilder.event(distinctId, eventName, props)
val delivery = ClientDelivery()
delivery.addMessage(message)
mixpanelAPI.deliver(delivery)
}.onFailure {
logger.warn("Failed to track Mixpanel event '{}': {}", eventName, it.message)
}
}
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/org/gitanimals/core/event/NoOpEventLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.gitanimals.core.event

import org.slf4j.LoggerFactory

class NoOpEventLogger : EventLogger {

private val logger = LoggerFactory.getLogger(this::class.java)

override fun track(eventName: String, distinctId: String, properties: Map<String, Any?>) {
logger.debug("NoOpEventLogger.track: event={}, distinctId={}", eventName, distinctId)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.gitanimals.gotcha.controller

import org.gitanimals.core.auth.InternalAuth
import org.gitanimals.core.auth.RequiredUserEntryPoints
import org.gitanimals.core.auth.UserEntryPoint
import org.gitanimals.core.event.EventLogger
import org.gitanimals.gotcha.app.GotchaFacadeV3
import org.gitanimals.gotcha.app.response.GotchaResponseV3
import org.gitanimals.gotcha.controller.response.ErrorResponse
Expand All @@ -13,6 +15,8 @@ import org.springframework.web.bind.annotation.*
@RestController
class GotchaController(
private val gotchaFacadeV3: GotchaFacadeV3,
private val eventLogger: EventLogger,
private val internalAuth: InternalAuth,
) {

@RequiredUserEntryPoints([UserEntryPoint.GITHUB])
Expand All @@ -26,6 +30,20 @@ class GotchaController(

val gotchaResponses = gotchaFacadeV3.gotcha(token, gotchaType, count)

internalAuth.findUserId()?.let { userId ->
gotchaResponses.forEach { response ->
eventLogger.track(
eventName = "complete_gotcha",
distinctId = userId.toString(),
properties = mapOf(
"pet_persona" to response.name,
"cost_point" to gotchaType.point,
"user_id" to userId,
),
)
}
}

return mapOf("gotchaResults" to gotchaResponses)
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/org/gitanimals/identity/app/AppleLoginFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.Jwts
import org.gitanimals.core.AUTHORIZATION_EXCEPTION
import org.gitanimals.core.event.EventLogger
import org.gitanimals.identity.app.AppleOauth2Api.AppleAuthKeyResponse
import org.gitanimals.identity.domain.EntryPoint
import org.gitanimals.identity.domain.UserService
Expand All @@ -23,6 +24,7 @@ class AppleLoginFacade(
private val appleOauth2Api: AppleOauth2Api,
private val objectMapper: ObjectMapper,
@Value("\${login.secret}") private val loginSecret: String,
private val eventLogger: EventLogger,
) {

private val logger = LoggerFactory.getLogger(this::class.simpleName)
Expand All @@ -49,6 +51,16 @@ class AppleLoginFacade(
}
}

eventLogger.track(
eventName = "complete_login",
distinctId = user.id.toString(),
properties = mapOf(
"provider" to "apple",
"user_id" to user.id,
"is_new_user" to !isExistsUser,
),
)

return tokenManager.createToken(user).withType()
}

Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/org/gitanimals/identity/app/GithubLoginFacade.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.gitanimals.identity.app

import org.gitanimals.core.event.EventLogger
import org.gitanimals.identity.domain.EntryPoint
import org.gitanimals.identity.domain.UserService
import org.springframework.stereotype.Service
Expand All @@ -10,11 +11,13 @@ class GithubLoginFacade(
private val userService: UserService,
private val contributionApi: ContributionApi,
private val tokenManager: TokenManager,
private val eventLogger: EventLogger,
) {

fun login(code: String): String {
val oauthUserResponse = githubOauth2Api.getOauthUsername(githubOauth2Api.getToken(code))

var isNewUser = false
val user = when (userService.existsByEntryPointAndAuthenticationId(
entryPoint = EntryPoint.GITHUB,
authenticationId = oauthUserResponse.id,
Expand Down Expand Up @@ -58,6 +61,7 @@ class GithubLoginFacade(
entryPoint = EntryPoint.GITHUB,
)
} else {
isNewUser = true
val contributedYears =
contributionApi.getAllContributionYearsWithToken(oauthUserResponse.username)
val contributionCountPerYears =
Expand All @@ -77,6 +81,16 @@ class GithubLoginFacade(
}
}

eventLogger.track(
eventName = "complete_login",
distinctId = user.id.toString(),
properties = mapOf(
"provider" to "github",
"user_id" to user.id,
"is_new_user" to isNewUser,
),
)

return tokenManager.createToken(user).withType()
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/org/gitanimals/quiz/app/SolveQuizFacade.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.gitanimals.quiz.app

import org.gitanimals.core.auth.InternalAuth
import org.gitanimals.core.event.EventLogger
import org.gitanimals.quiz.app.request.CreateSolveQuizRequest
import org.gitanimals.quiz.app.response.QuizContextResponse
import org.gitanimals.quiz.app.response.TodaySolvedContextResponse
import org.gitanimals.quiz.domain.approved.QuizService
import org.gitanimals.quiz.domain.context.QuizSolveContext
import org.gitanimals.quiz.domain.context.QuizSolveContextService
import org.gitanimals.quiz.domain.context.QuizSolveContextStatus
import org.gitanimals.quiz.domain.core.Language
import org.gitanimals.quiz.domain.core.Level
import org.springframework.stereotype.Service
Expand All @@ -16,6 +18,7 @@ class SolveQuizFacade(
private val internalAuth: InternalAuth,
private val quizService: QuizService,
private val quizSolveContextService: QuizSolveContextService,
private val eventLogger: EventLogger,
) {

fun createContext(locale: String, request: CreateSolveQuizRequest): Long {
Expand Down Expand Up @@ -54,6 +57,17 @@ class SolveQuizFacade(
val userId = internalAuth.getUserId()

quizSolveContextService.solveQuiz(id, userId, answer)

val context = quizSolveContextService.getQuizSolveContextByIdAndUserId(id, userId)
eventLogger.track(
eventName = "submit_quiz_answer",
distinctId = userId.toString(),
properties = mapOf(
"quiz_id" to id,
"is_correct" to (context.getStatus() in setOf(QuizSolveContextStatus.SUCCESS, QuizSolveContextStatus.DONE)),
"user_id" to userId,
)
)
}

fun getQuizById(id: Long): QuizSolveContext {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.gitanimals.quiz.infra.hibernate

import org.gitanimals.core.event.EventLogger
import org.gitanimals.quiz.domain.approved.Quiz
import org.gitanimals.quiz.infra.event.NewQuizCreated
import org.hibernate.event.spi.PostInsertEvent
Expand All @@ -12,6 +13,7 @@ import org.springframework.stereotype.Component
@Component
class NewQuizCreatedInsertHibernateEventListener(
private val applicationEventPublisher: ApplicationEventPublisher,
private val eventLogger: EventLogger,
) : PostInsertEventListener {

private val logger = LoggerFactory.getLogger(this::class.simpleName)
Expand All @@ -21,13 +23,23 @@ class NewQuizCreatedInsertHibernateEventListener(

override fun onPostInsert(event: PostInsertEvent) {
if (event.entity is Quiz) {
val quiz = event.entity as Quiz
runCatching {
applicationEventPublisher.publishEvent(
NewQuizCreated.from(event.entity as Quiz)
NewQuizCreated.from(quiz)
)
}.onFailure {
logger.error("Cannot publish NewQuizCreate event. cause ${it.message}", it)
}
eventLogger.track(
eventName = "complete_make_quiz",
distinctId = quiz.userId.toString(),
properties = mapOf(
"quiz_id" to quiz.id,
"language" to quiz.language.name,
"user_id" to quiz.userId,
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.gitanimals.quiz.infra.hibernate
import org.gitanimals.core.GracefulShutdownDispatcher.gracefulLaunch
import org.gitanimals.core.IdGenerator
import org.gitanimals.core.clock
import org.gitanimals.core.event.EventLogger
import org.gitanimals.inbox.domain.InboxType
import org.gitanimals.quiz.app.IdentityApi
import org.gitanimals.quiz.app.InboxApi
Expand Down Expand Up @@ -34,9 +35,11 @@ class QuizSolveContextDoneHibernateEventListener(
if (quizSolveContext.getStatus() == QuizSolveContextStatus.DONE) {
applicationEventPublisher.publishEvent(
QuizSolveContextDoneLogicDelegator.QuizSolveContextDone(
contextId = quizSolveContext.id,
userId = quizSolveContext.userId,
prize = quizSolveContext.getPrize(),
status = quizSolveContext.getStatus(),
language = quizSolveContext.category.name,
)
)
}
Expand All @@ -47,14 +50,17 @@ class QuizSolveContextDoneHibernateEventListener(
class QuizSolveContextDoneLogicDelegator(
private val inboxApi: InboxApi,
private val identityApi: IdentityApi,
private val eventLogger: EventLogger,
) {

private val logger = LoggerFactory.getLogger(this::class.simpleName)

data class QuizSolveContextDone(
val contextId: Long,
val userId: Long,
val prize: Int,
val status: QuizSolveContextStatus,
val language: String,
)

@EventListener(QuizSolveContextDone::class)
Expand Down Expand Up @@ -91,6 +97,17 @@ class QuizSolveContextDoneLogicDelegator(
it
)
}

eventLogger.track(
eventName = "complete_solve_quiz",
distinctId = event.userId.toString(),
properties = mapOf(
"context_id" to event.contextId,
"score" to event.prize,
"language" to event.language,
"user_id" to event.userId,
)
)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ tokenizer.api.key=foo
quiz.approve.token=
relay.approve.token=
gitanimals.admin.token=

### Mixpanel ###
mixpanel.project.token=${MIXPANEL_PROJECT_TOKEN:}
Loading