Skip to content

Commit

Permalink
feat: PostHog integration (#1788)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanCizmar authored Jul 13, 2023
1 parent 02b6808 commit a006016
Show file tree
Hide file tree
Showing 51 changed files with 6,433 additions and 4,764 deletions.
1 change: 1 addition & 0 deletions backend/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {
implementation libs.jjwtImpl
implementation libs.jjwtJackson
implementation("com.github.ben-manes.caffeine:caffeine:3.0.5")
api libs.postHog
}

sourceSets {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.tolgee.api.v2.controllers

import io.sentry.Sentry
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.component.reporting.BusinessEventPublisher
import io.tolgee.dtos.request.BusinessEventReportRequest
import io.tolgee.service.organization.OrganizationRoleService
import io.tolgee.service.security.SecurityService
import io.tolgee.util.Logging
import io.tolgee.util.logger
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/business-events"])
@Tag(name = "Business events reporting")
class BusinessEventController(
private val businessEventPublisher: BusinessEventPublisher,
private val securityService: SecurityService,
private val organizationRoleService: OrganizationRoleService,
) : Logging {
@PostMapping("/report")
@Operation(summary = "Reports business event")
fun report(@RequestBody eventData: BusinessEventReportRequest) {
try {
eventData.projectId?.let { securityService.checkAnyProjectPermission(it) }
eventData.organizationId?.let { organizationRoleService.checkUserCanView(it) }
businessEventPublisher.publish(eventData)
} catch (e: Throwable) {
logger.error("Error storing event", e)
Sentry.captureException(e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ class V2ImportController(
private val projectHolder: ProjectHolder,
private val languageService: LanguageService,
private val namespaceService: NamespaceService,

) {
@PostMapping("", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@AccessWithAnyProjectPermission()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ class V2ExportController(
.toList()
.map { language -> language.tag }
.toSet()

val exported = exportService.export(projectHolder.project.id, params)
checkExportNotEmpty(exported)
return getExportResponse(params, exported)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class PublicConfigurationDTO(
properties: TolgeeProperties,
val machineTranslationServices: MtServicesDTO,
val billing: PublicBillingConfigurationDTO,
val version: String
val version: String,
) {
val authentication: Boolean = properties.authentication.enabled
var authMethods: AuthMethodsDTO? = null
Expand All @@ -31,6 +31,8 @@ class PublicConfigurationDTO(
val chatwootToken = properties.chatwootToken
val capterraTracker = properties.capterraTracker
val ga4Tag = properties.ga4Tag
val postHogApiKey: String? = properties.postHog.apiKey
val postHogHost: String? = properties.postHog.host

class AuthMethodsDTO(
val github: OAuthPublicConfigDTO,
Expand Down
4 changes: 0 additions & 4 deletions backend/app/src/test/kotlin/io/tolgee/HealthCheckTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ import org.springframework.transaction.annotation.Transactional
@ContextRecreatingTest
class HealthCheckTest : AbstractControllerTest() {

// @Autowired
// @MockBean(answer = Answers.CALLS_REAL_METHODS)
// private lateinit var ds: DataSource

@Test
fun `health check works`() {
performGet("/actuator/health").andIsOk
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
package io.tolgee.activity

import com.posthog.java.PostHog
import io.tolgee.ProjectAuthControllerTest
import io.tolgee.development.testDataBuilder.data.BaseTestData
import io.tolgee.fixtures.AuthorizedRequestFactory
import io.tolgee.fixtures.andAssertThatJson
import io.tolgee.fixtures.andIsOk
import io.tolgee.fixtures.andPrettyPrint
import io.tolgee.fixtures.isValidId
import io.tolgee.fixtures.node
import io.tolgee.fixtures.waitForNotThrowing
import io.tolgee.testing.annotations.ProjectJWTAuthTestMethod
import io.tolgee.testing.assert
import net.javacrumbs.jsonunit.assertj.JsonAssert
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.HttpHeaders
import java.math.BigDecimal

class ActivityLogTest : ProjectAuthControllerTest("/v2/projects/") {

private lateinit var testData: BaseTestData

@MockBean
@Autowired
lateinit var postHog: PostHog

@BeforeEach
fun setup() {
Mockito.reset(postHog)
testData = BaseTestData()
testData.user.name = "Franta"
testData.projectBuilder.apply {
Expand All @@ -36,7 +54,8 @@ class ActivityLogTest : ProjectAuthControllerTest("/v2/projects/") {
fun `it stores and returns translation set activity`() {
performProjectAuthPut(
"translations",
mapOf("key" to "key", "translations" to mapOf(testData.englishLanguage.tag to "Test"))
mapOf("key" to "key", "translations" to mapOf(testData.englishLanguage.tag to "Test")),

).andIsOk

performProjectAuthGet("activity").andIsOk.andPrettyPrint.andAssertThatJson {
Expand All @@ -62,6 +81,31 @@ class ActivityLogTest : ProjectAuthControllerTest("/v2/projects/") {
}
}

@Test
@ProjectJWTAuthTestMethod
fun `it publishes business event to external monitor`() {
performPut(
"/v2/projects/${project.id}/translations",
mapOf("key" to "key", "translations" to mapOf(testData.englishLanguage.tag to "Test")),
HttpHeaders().also {
it["Authorization"] = listOf(AuthorizedRequestFactory.getBearerTokenString(generateJwtToken(userAccount!!.id)))
it["X-Tolgee-Utm"] = "eyJ1dG1faGVsbG8iOiJoZWxsbyJ9"
}
).andIsOk

var params: Map<String, Any?>? = null
waitForNotThrowing(timeout = 10000) {
verify(postHog, times(1)).capture(
any(), eq("SET_TRANSLATIONS"),
argThat {
params = this
true
}
)
}
params!!["utm_hello"].assert.isEqualTo("hello")
}

private fun JsonAssert.isValidTranslationModifications() {
node("auto") {
node("old").isEqualTo(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.tolgee.api.v2.controllers

import com.posthog.java.PostHog
import io.tolgee.ProjectAuthControllerTest
import io.tolgee.development.testDataBuilder.data.BaseTestData
import io.tolgee.fixtures.AuthorizedRequestFactory
import io.tolgee.fixtures.andIsOk
import io.tolgee.fixtures.waitForNotThrowing
import io.tolgee.testing.annotations.ProjectJWTAuthTestMethod
import io.tolgee.testing.assert
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.HttpHeaders

class BusinessEventControllerTest : ProjectAuthControllerTest("/v2/projects/") {
private lateinit var testData: BaseTestData

@MockBean
@Autowired
lateinit var postHog: PostHog

@BeforeEach
fun setup() {
testData = BaseTestData()
testDataService.saveTestData(testData.root)
projectSupplier = { testData.projectBuilder.self }
userAccount = testData.user
}

@Test
@ProjectJWTAuthTestMethod
fun `it publishes business event to external monitor`() {
performPost(
"/v2/business-events/report",
mapOf(
"eventName" to "TEST_EVENT",
"organizationId" to testData.userAccountBuilder.defaultOrganizationBuilder.self.id,
"projectId" to testData.projectBuilder.self.id,
"data" to mapOf("test" to "test")
),
HttpHeaders().also {
it["Authorization"] = listOf(AuthorizedRequestFactory.getBearerTokenString(generateJwtToken(userAccount!!.id)))
it["X-Tolgee-Utm"] = "eyJ1dG1faGVsbG8iOiJoZWxsbyJ9"
}
).andIsOk

var params: Map<String, Any?>? = null
waitForNotThrowing(timeout = 10000) {
verify(postHog, times(1)).capture(
any(), eq("TEST_EVENT"),
argThat {
params = this
true
}
)
}
params!!["utm_hello"].assert.isEqualTo("hello")
params!!["organizationId"].assert.isNotNull
params!!["organizationName"].assert.isEqualTo("test_username")
params!!["utm_hello"].assert.isEqualTo("hello")
params!!["test"].assert.isEqualTo("test")
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package io.tolgee.controllers

import com.posthog.java.PostHog
import io.tolgee.dtos.misc.CreateProjectInvitationParams
import io.tolgee.dtos.request.auth.SignUpDto
import io.tolgee.fixtures.andAssertResponse
import io.tolgee.fixtures.andIsBadRequest
import io.tolgee.fixtures.andIsOk
import io.tolgee.fixtures.generateUniqueString
import io.tolgee.fixtures.waitForNotThrowing
import io.tolgee.model.enums.ProjectPermissionType
import io.tolgee.testing.AbstractControllerTest
import io.tolgee.testing.assertions.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import kotlin.properties.Delegates

@SpringBootTest
@AutoConfigureMockMvc
class PublicControllerTest :
AbstractControllerTest() {
Expand All @@ -25,6 +32,7 @@ class PublicControllerTest :

@BeforeEach
fun setup() {
Mockito.reset(postHog)
canCreateOrganizations = tolgeeProperties.authentication.userCanCreateOrganizations
}

Expand All @@ -33,6 +41,10 @@ class PublicControllerTest :
tolgeeProperties.authentication.userCanCreateOrganizations = canCreateOrganizations
}

@MockBean
@Autowired
lateinit var postHog: PostHog

@Test
fun `creates organization`() {
val dto = SignUpDto(
Expand All @@ -52,6 +64,15 @@ class PublicControllerTest :
assertThat(organizationRepository.findAllByName("Pavel Novak")).hasSize(1)
}

@Test
fun `logs event to post hog`() {
val dto = SignUpDto(name = "Pavel Novak", password = "aaaaaaaaa", email = "[email protected]")
performPost("/api/public/sign_up", dto).andIsOk
waitForNotThrowing(timeout = 10000) {
verify(postHog, times(1)).capture(any(), eq("SIGN_UP"), any())
}
}

@Test
fun `doesn't create organization when invitation provided`() {
val base = dbPopulator.createBase(generateUniqueString())
Expand Down
2 changes: 2 additions & 0 deletions backend/data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-configuration-processor"
implementation "org.springframework.boot:spring-boot-starter-batch"


/**
* DB
*/
Expand Down Expand Up @@ -165,6 +166,7 @@ dependencies {
implementation libs.hibernateTypes
liquibaseRuntime libs.hibernateTypes
implementation 'com.eatthepath:java-otp:0.4.0'
implementation libs.postHog

/**
* Google translation API
Expand Down
31 changes: 31 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/activity/ActivityFilter.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package io.tolgee.activity

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.sentry.Sentry
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.*
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
Expand All @@ -26,11 +31,37 @@ class ActivityFilter(
activityHolder.activity = activityAnnotation.activity
}

assignUtmDataHolder(request)

filterChain.doFilter(request, response)
}

private fun getActivityAnnotation(request: HttpServletRequest): RequestActivity? {
val handlerMethod = (requestMappingHandlerMapping.getHandler(request)?.handler as HandlerMethod?)
return handlerMethod?.getMethodAnnotation(RequestActivity::class.java)
}

fun assignUtmDataHolder(request: HttpServletRequest) {
try {
val headerValue = request.getHeader(UTM_HEADER_NAME) ?: return
val parsed = parseUtmValues(headerValue) ?: return
activityHolder.utmData = parsed
} catch (e: Exception) {
Sentry.captureException(e)
logger.error(e)
}
}

fun parseUtmValues(headerValue: String?): Map<String, Any?>? {
val urlDecoded = URLDecoder.decode(headerValue, StandardCharsets.UTF_8)
val base64Decoded = Base64.getDecoder().decode(urlDecoded)
val utmParamsJson = String(base64Decoded, StandardCharsets.UTF_8)
val utmParams = mutableMapOf<String, String>()
return jacksonObjectMapper().readValue(utmParamsJson, utmParams::class.java)
.filterKeys { it.startsWith("utm_") }
}

companion object {
const val UTM_HEADER_NAME = "X-Tolgee-Utm"
}
}
Loading

0 comments on commit a006016

Please sign in to comment.