From 6f279d1442b7d2c48cbcf8969b95dc968527ff64 Mon Sep 17 00:00:00 2001 From: Anatoly Kislov Date: Thu, 28 Sep 2017 05:49:06 +0300 Subject: [PATCH 1/4] feat: google analytics --- build.gradle | 8 +- src/main/kotlin/app/Analytics.kt | 95 +++++++++++++++++++ src/main/kotlin/app/Logger.kt | 4 + src/main/kotlin/app/Main.kt | 11 +++ src/main/kotlin/app/api/ServerApi.kt | 41 +++++--- src/main/kotlin/app/config/Config.kt | 1 + src/main/kotlin/app/config/Configurator.kt | 1 + .../kotlin/app/config/FileConfigurator.kt | 19 ++++ .../kotlin/app/config/MockConfigurator.kt | 7 +- src/main/kotlin/app/hashers/RepoHasher.kt | 3 + src/main/kotlin/app/ui/AddRepoState.kt | 3 + src/main/kotlin/app/ui/AuthState.kt | 5 + src/main/kotlin/app/ui/UpdateRepoState.kt | 3 + 13 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/app/Analytics.kt diff --git a/build.gradle b/build.gradle index 865c0621..4d16a284 100644 --- a/build.gradle +++ b/build.gradle @@ -29,8 +29,12 @@ buildConfig { packageName = 'app' buildConfigField 'int', 'VERSION_CODE', '1' buildConfigField 'String', 'VERSION', '0.0.1' - buildConfigField "String", "PROFILE_URL", "https://sourcerer.io/" - buildConfigField "String", "API_BASE_URL", "http://localhost:3181" + buildConfigField 'String', 'PROFILE_URL', 'https://sourcerer.io/' + buildConfigField 'String', 'API_BASE_URL', 'http://localhost:3181' + buildConfigField 'String', 'GA_BASE_URL', 'http://www.google-analytics.com' + buildConfigField 'String', 'GA_TRACKING_ID', 'UA-107129190-2' + buildConfigField 'boolean', 'IS_GA_ENABLED', 'true' + buildConfig } junitPlatform { diff --git a/src/main/kotlin/app/Analytics.kt b/src/main/kotlin/app/Analytics.kt new file mode 100644 index 00000000..a0f595fb --- /dev/null +++ b/src/main/kotlin/app/Analytics.kt @@ -0,0 +1,95 @@ +package app + +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.Method +import com.github.kittinunf.fuel.core.Request + +typealias Param = Pair + +/** + * Google Analytics events tracking. + */ +object Analytics { + private val IS_ENABLED = BuildConfig.IS_GA_ENABLED + private val BASE_PATH = BuildConfig.GA_BASE_URL + private val PROTOCOL_VERSION = "1" + private val TRACKING_ID = BuildConfig.GA_TRACKING_ID + private val DATA_SOURCE = "app" + + private val HIT_PAGEVIEW = "pageview" + private val HIT_EXCEPTION = "exception" + + private val fuelManager = FuelManager() + + var uuid: String = "" // Should be set on start of the app. + var username: String = "" // Should be set on success authorization. + + init { + fuelManager.basePath = BASE_PATH + } + + private fun post(params: List): Request { + return fuelManager.request(Method.POST, "/collect", params) + } + + private fun trackEvent(event: String, params: List = listOf()) { + if (!IS_ENABLED || (username.isEmpty() && uuid.isEmpty())) { + return + } + + val idParams = mutableListOf() + if (uuid.isNotEmpty()) { + idParams.add("cid" to uuid) + } + if (username.isNotEmpty()) { + idParams.add("uid" to username) + } + + val defaultParams = listOf("v" to PROTOCOL_VERSION, + "tid" to TRACKING_ID, + "ds" to DATA_SOURCE, + "t" to HIT_PAGEVIEW, + "dp" to event) + + try { + // Send event to GA with united params. + post(params + defaultParams.filter { !params.contains(it) } + + idParams).responseString() + } catch (e: Throwable) { + // Never fail from analytics. + } + } + + fun trackStart() { + trackEvent("/start") + } + + fun trackAuth() { + trackEvent("/auth") + } + + fun trackConfigSetup() { + trackEvent("/config/setup") + } + + fun trackConfigChanged() { + trackEvent("/config/changed") + } + + fun trackHashingRepoSuccess() { + trackEvent("/hashing/repo/success") + } + + fun trackHashingSuccess() { + trackEvent("/hashing/success") + } + + fun trackError() { + // TODO(anatoly): Send message with exd, consider limit 150 bytes. + trackEvent("/error", listOf("t" to HIT_EXCEPTION)) + } + + fun trackEnd() { + trackEvent("/end") + } +} diff --git a/src/main/kotlin/app/Logger.kt b/src/main/kotlin/app/Logger.kt index 1cc15a2f..0e31cfca 100644 --- a/src/main/kotlin/app/Logger.kt +++ b/src/main/kotlin/app/Logger.kt @@ -39,6 +39,8 @@ object Logger { if (LEVEL >= ERROR) { println("[e] $message.") } + + Analytics.trackError() // May be not clear, but affects all. } /** @@ -48,6 +50,8 @@ object Logger { if (LEVEL >= ERROR) { println("[e] $message: $e") } + + Analytics.trackError() } /** diff --git a/src/main/kotlin/app/Main.kt b/src/main/kotlin/app/Main.kt index 88122558..38efa2ce 100644 --- a/src/main/kotlin/app/Main.kt +++ b/src/main/kotlin/app/Main.kt @@ -27,6 +27,9 @@ class Main(argv: Array) { private val api = ServerApi(configurator) init { + Analytics.uuid = configurator.getUuidPersistent() + Analytics.trackStart() + val options = Options() val commandAdd = CommandAdd() val commandConfig = CommandConfig() @@ -60,6 +63,8 @@ class Main(argv: Array) { } catch (e: MissingCommandException) { Logger.error("No such command: ${e.unknownCommand}") } + + Analytics.trackEnd() } private fun startUi() { @@ -74,6 +79,8 @@ class Main(argv: Array) { configurator.addLocalRepoPersistent(localRepo) configurator.saveToFile() println("Added git repository at $path.") + + Analytics.trackConfigChanged() } else { Logger.error("No valid git repository found at $path.") } @@ -93,6 +100,8 @@ class Main(argv: Array) { } configurator.saveToFile() + + Analytics.trackConfigChanged() } private fun doList() { @@ -108,6 +117,8 @@ class Main(argv: Array) { configurator.removeLocalRepoPersistent(LocalRepo(path)) configurator.saveToFile() println("Repository removed from tracking list.") + + Analytics.trackConfigChanged() } else { println("Repository not found in tracking list.") } diff --git a/src/main/kotlin/app/api/ServerApi.kt b/src/main/kotlin/app/api/ServerApi.kt index a375b046..f0899705 100644 --- a/src/main/kotlin/app/api/ServerApi.kt +++ b/src/main/kotlin/app/api/ServerApi.kt @@ -13,8 +13,8 @@ import app.model.FactGroup import app.model.Repo import app.model.User import app.utils.RequestException -import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.Method import com.github.kittinunf.fuel.core.Request import com.github.kittinunf.fuel.core.Response import com.google.protobuf.InvalidProtocolBufferException @@ -30,6 +30,7 @@ class ServerApi (private val configurator: Configurator) : Api { private val KEY_TOKEN = "Token=" } + private val fuelManager = FuelManager() private var token = "" private fun cookieRequestInterceptor() = { req: Request -> @@ -50,7 +51,6 @@ class ServerApi (private val configurator: Configurator) : Api { } init { - val fuelManager = FuelManager.instance fuelManager.basePath = BuildConfig.API_BASE_URL fuelManager.addRequestInterceptor { cookieRequestInterceptor() } fuelManager.addResponseInterceptor { cookieResponseInterceptor() } @@ -62,38 +62,49 @@ class ServerApi (private val configurator: Configurator) : Api { private val password get() = configurator.getPassword() + private fun post(path: String): Request { + return fuelManager.request(Method.POST, path) + } + + private fun get(path: String): Request { + return fuelManager.request(Method.GET, path) + } + + private fun delete(path: String): Request { + return fuelManager.request(Method.DELETE, path) + } + private fun createRequestGetToken(): Request { - return Fuel.post("/auth").authenticate(username, password) + return post("/auth").authenticate(username, password) .header(getVersionCodeHeader()) } private fun createRequestGetUser(): Request { - return Fuel.get("/user") + return get("/user") } private fun createRequestGetRepo(repoRehash: String): Request { - return Fuel.get("/repo/$repoRehash") + return get("/repo/$repoRehash") } private fun createRequestPostRepo(repo: Repo): Request { - return Fuel.post("/repo").header(getContentTypeHeader()) - .body(repo.serialize()) + return post("/repo").header(getContentTypeHeader()) + .body(repo.serialize()) } private fun createRequestPostCommits(commits: CommitGroup): Request { - return Fuel.post("/commits").header(getContentTypeHeader()) - .body(commits.serialize()) + return post("/commits").header(getContentTypeHeader()) + .body(commits.serialize()) } private fun createRequestDeleteCommits(commits: CommitGroup): Request { - return Fuel.delete("/commits").header(getContentTypeHeader()) - .body(commits.serialize()) + return delete("/commits").header(getContentTypeHeader()) + .body(commits.serialize()) } - private fun createRequestPostFacts(facts: FactGroup): - Request { - return Fuel.post("/facts").header(getContentTypeHeader()) - .body(facts.serialize()) + private fun createRequestPostFacts(facts: FactGroup): Request { + return post("/facts").header(getContentTypeHeader()) + .body(facts.serialize()) } private fun makeRequest(request: Request, diff --git a/src/main/kotlin/app/config/Config.kt b/src/main/kotlin/app/config/Config.kt index cf1cabca..98b4fa51 100644 --- a/src/main/kotlin/app/config/Config.kt +++ b/src/main/kotlin/app/config/Config.kt @@ -10,6 +10,7 @@ import app.utils.Options * Config data class. */ class Config ( + var uuid: String = "", var username: String = "", var password: String = "", var localRepos: MutableSet = mutableSetOf() diff --git a/src/main/kotlin/app/config/Configurator.kt b/src/main/kotlin/app/config/Configurator.kt index a54033ac..0cab1036 100644 --- a/src/main/kotlin/app/config/Configurator.kt +++ b/src/main/kotlin/app/config/Configurator.kt @@ -16,6 +16,7 @@ interface Configurator { fun getRepos(): List fun setUsernameCurrent(username: String) fun setPasswordCurrent(password: String) + fun getUuidPersistent(): String fun setUsernamePersistent(username: String) fun setPasswordPersistent(password: String) fun addLocalRepoPersistent(localRepo: LocalRepo) diff --git a/src/main/kotlin/app/config/FileConfigurator.kt b/src/main/kotlin/app/config/FileConfigurator.kt index 684873d6..117e9b22 100644 --- a/src/main/kotlin/app/config/FileConfigurator.kt +++ b/src/main/kotlin/app/config/FileConfigurator.kt @@ -21,6 +21,7 @@ import java.nio.file.Files import java.nio.file.InvalidPathException import java.nio.file.NoSuchFileException import java.nio.file.Paths +import java.util.UUID /** * Singleton class that manage configs and CLI options. @@ -85,6 +86,17 @@ class FileConfigurator : Configurator { */ init { loadFromFile() + assignUuidIfMissing() + } + + /** + * Generates UUID for analytics on install. + */ + private fun assignUuidIfMissing() { + if (persistent.uuid.isNotEmpty()) { + return + } + persistent.uuid = UUID.randomUUID().toString() } /** @@ -152,6 +164,13 @@ class FileConfigurator : Configurator { current.password = PasswordHelper.hashPassword(password) } + /** + * Gets UUID. + */ + override fun getUuidPersistent(): String { + return persistent.uuid + } + /** * Sets username to persistent config. Use [saveToFile] to save. */ diff --git a/src/main/kotlin/app/config/MockConfigurator.kt b/src/main/kotlin/app/config/MockConfigurator.kt index 521401bb..1b482c23 100644 --- a/src/main/kotlin/app/config/MockConfigurator.kt +++ b/src/main/kotlin/app/config/MockConfigurator.kt @@ -13,7 +13,8 @@ class MockConfigurator(var mockUsername: String = "", var mockIsFirstLaunch: Boolean = true, var mockRepos: MutableList = mutableListOf(), var mockLocalRepos: MutableList = - mutableListOf()) : Configurator { + mutableListOf(), + var uuid: String = "") : Configurator { var mockCurrent: Config = Config() var mockPersistent: Config = Config() var mockOptions: Options = Options() @@ -50,6 +51,10 @@ class MockConfigurator(var mockUsername: String = "", mockCurrent.password = password } + override fun getUuidPersistent(): String { + return uuid + } + override fun setUsernamePersistent(username: String) { mockPersistent.username = username } diff --git a/src/main/kotlin/app/hashers/RepoHasher.kt b/src/main/kotlin/app/hashers/RepoHasher.kt index 19dc2e18..b288c07b 100644 --- a/src/main/kotlin/app/hashers/RepoHasher.kt +++ b/src/main/kotlin/app/hashers/RepoHasher.kt @@ -3,6 +3,7 @@ package app.hashers +import app.Analytics import app.Logger import app.api.Api import app.config.Configurator @@ -61,6 +62,8 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, // Confirm hashing completion. postRepoToServer() + + Analytics.trackHashingRepoSuccess() } finally { closeGit(git) diff --git a/src/main/kotlin/app/ui/AddRepoState.kt b/src/main/kotlin/app/ui/AddRepoState.kt index bd89671f..e36156db 100644 --- a/src/main/kotlin/app/ui/AddRepoState.kt +++ b/src/main/kotlin/app/ui/AddRepoState.kt @@ -3,6 +3,7 @@ package app.ui +import app.Analytics import app.api.Api import app.config.Configurator import app.model.LocalRepo @@ -44,6 +45,8 @@ class AddRepoState constructor(private val context: Context, } } } + + Analytics.trackConfigSetup() } override fun next() { diff --git a/src/main/kotlin/app/ui/AuthState.kt b/src/main/kotlin/app/ui/AuthState.kt index a68f2ccf..627b63e1 100644 --- a/src/main/kotlin/app/ui/AuthState.kt +++ b/src/main/kotlin/app/ui/AuthState.kt @@ -3,6 +3,7 @@ package app.ui +import app.Analytics import app.BuildConfig import app.api.Api import app.config.Configurator @@ -74,6 +75,10 @@ class AuthState constructor(private val context: Context, println("You are successfully authenticated. Your profile page is " + BuildConfig.PROFILE_URL + configurator.getUsername()) saveCredentialsIfChanged() + + Analytics.username = configurator.getUsername() + Analytics.trackAuth() + return true } catch (e: RequestException) { if (e.isAuthError) { diff --git a/src/main/kotlin/app/ui/UpdateRepoState.kt b/src/main/kotlin/app/ui/UpdateRepoState.kt index 58feeede..68c86320 100644 --- a/src/main/kotlin/app/ui/UpdateRepoState.kt +++ b/src/main/kotlin/app/ui/UpdateRepoState.kt @@ -3,6 +3,7 @@ package app.ui +import app.Analytics import app.hashers.RepoHasher import app.Logger import app.api.Api @@ -29,6 +30,8 @@ class UpdateRepoState constructor(private val context: Context, } println("The repositories have been hashed. See result online on your " + "Sourcerer profile.") + + Analytics.trackHashingSuccess() } override fun next() { From d9c48efaf40f74ea9c2f928306175ba901543f3d Mon Sep 17 00:00:00 2001 From: Anatoly Kislov Date: Sat, 30 Sep 2017 00:49:18 +0300 Subject: [PATCH 2/4] wip: pr --- build.gradle | 2 +- src/main/kotlin/app/Analytics.kt | 40 ++++++++++++++++++++++---------- src/main/kotlin/app/Main.kt | 2 +- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 4d16a284..ba01cd11 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ buildConfig { buildConfigField 'String', 'VERSION', '0.0.1' buildConfigField 'String', 'PROFILE_URL', 'https://sourcerer.io/' buildConfigField 'String', 'API_BASE_URL', 'http://localhost:3181' - buildConfigField 'String', 'GA_BASE_URL', 'http://www.google-analytics.com' + buildConfigField 'String', 'GA_BASE_PATH', 'http://www.google-analytics.com' buildConfigField 'String', 'GA_TRACKING_ID', 'UA-107129190-2' buildConfigField 'boolean', 'IS_GA_ENABLED', 'true' buildConfig diff --git a/src/main/kotlin/app/Analytics.kt b/src/main/kotlin/app/Analytics.kt index a0f595fb..ad9d833c 100644 --- a/src/main/kotlin/app/Analytics.kt +++ b/src/main/kotlin/app/Analytics.kt @@ -1,3 +1,6 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + package app import com.github.kittinunf.fuel.core.FuelManager @@ -11,7 +14,8 @@ typealias Param = Pair */ object Analytics { private val IS_ENABLED = BuildConfig.IS_GA_ENABLED - private val BASE_PATH = BuildConfig.GA_BASE_URL + private val BASE_PATH = BuildConfig.GA_BASE_PATH + private val BASE_URL = "/virtual/app/" private val PROTOCOL_VERSION = "1" private val TRACKING_ID = BuildConfig.GA_TRACKING_ID private val DATA_SOURCE = "app" @@ -22,7 +26,7 @@ object Analytics { private val fuelManager = FuelManager() var uuid: String = "" // Should be set on start of the app. - var username: String = "" // Should be set on success authorization. + var username: String = "" // Should be set on successful authorization. init { fuelManager.basePath = BASE_PATH @@ -32,6 +36,18 @@ object Analytics { return fuelManager.request(Method.POST, "/collect", params) } + /** + * Google Analytics Measurement Protocol is used to track events. + * User iteration data is sent to GA endpoint via POST request. + * Events (or hits) mapped to virtual urls with "Data Source" parameter. + * Used parameters: + * - v: Protocol Version (Required) + * - tid: Tracking ID - used to specify GA account (Required) + * - cid: Client ID - anonymous client id (UUID type 4) + * - uid: User ID - username + * - t: Hit Type - type of event + * - dp: Document Path - virtual url + */ private fun trackEvent(event: String, params: List = listOf()) { if (!IS_ENABLED || (username.isEmpty() && uuid.isEmpty())) { return @@ -49,7 +65,7 @@ object Analytics { "tid" to TRACKING_ID, "ds" to DATA_SOURCE, "t" to HIT_PAGEVIEW, - "dp" to event) + "dp" to BASE_URL + event) try { // Send event to GA with united params. @@ -61,35 +77,35 @@ object Analytics { } fun trackStart() { - trackEvent("/start") + trackEvent("start") } fun trackAuth() { - trackEvent("/auth") + trackEvent("auth") } fun trackConfigSetup() { - trackEvent("/config/setup") + trackEvent("config/setup") } fun trackConfigChanged() { - trackEvent("/config/changed") + trackEvent("config/changed") } fun trackHashingRepoSuccess() { - trackEvent("/hashing/repo/success") + trackEvent("hashing/repo/success") } fun trackHashingSuccess() { - trackEvent("/hashing/success") + trackEvent("hashing/success") } fun trackError() { // TODO(anatoly): Send message with exd, consider limit 150 bytes. - trackEvent("/error", listOf("t" to HIT_EXCEPTION)) + trackEvent("error", listOf("t" to HIT_EXCEPTION)) } - fun trackEnd() { - trackEvent("/end") + fun trackExit() { + trackEvent("exit") } } diff --git a/src/main/kotlin/app/Main.kt b/src/main/kotlin/app/Main.kt index 38efa2ce..9c31b435 100644 --- a/src/main/kotlin/app/Main.kt +++ b/src/main/kotlin/app/Main.kt @@ -64,7 +64,7 @@ class Main(argv: Array) { Logger.error("No such command: ${e.unknownCommand}") } - Analytics.trackEnd() + Analytics.trackExit() } private fun startUi() { From 6342e74c24f031991e45d309f839ec68cc8c8786 Mon Sep 17 00:00:00 2001 From: Anatoly Kislov Date: Sun, 1 Oct 2017 16:12:28 +0300 Subject: [PATCH 3/4] wip: error codes by virtual urls --- src/main/kotlin/app/Analytics.kt | 39 ++++++++++++++++++++++++++++++-- src/main/kotlin/app/Logger.kt | 4 ++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/app/Analytics.kt b/src/main/kotlin/app/Analytics.kt index ad9d833c..988be861 100644 --- a/src/main/kotlin/app/Analytics.kt +++ b/src/main/kotlin/app/Analytics.kt @@ -3,9 +3,12 @@ package app +import com.github.kittinunf.fuel.core.FuelError import com.github.kittinunf.fuel.core.FuelManager import com.github.kittinunf.fuel.core.Method import com.github.kittinunf.fuel.core.Request +import com.google.protobuf.InvalidProtocolBufferException +import java.security.InvalidParameterException typealias Param = Pair @@ -100,12 +103,44 @@ object Analytics { trackEvent("hashing/success") } - fun trackError() { + fun trackError(e: Throwable = Throwable()) { // TODO(anatoly): Send message with exd, consider limit 150 bytes. - trackEvent("error", listOf("t" to HIT_EXCEPTION)) + val url = getErrorUrl(e) + val separator = if (url.isNotEmpty()) "/" else "" + trackEvent("error" + separator + url, listOf("t" to HIT_EXCEPTION)) } fun trackExit() { trackEvent("exit") } + + private fun getErrorUrl(e: Throwable): String { + // Mapping for request exceptions. + when (e) { + is FuelError -> return "request" + is InvalidParameterException -> return "request/parsing" + is InvalidProtocolBufferException -> return "request/parsing" + } + + // Get concrete class of exception name removing all common parts. + val name = e.javaClass.simpleName.replace("Exception", "") + .replace("Error", "") + .replace("Throwable", "") + + if (name.length == 0 || name.length == 1) { + return name + } + + // Divide CamelCased words in class name by dashes. + val nameCapitalized = name.toUpperCase() + var url = name[0].toString() + for (i in 1..name.length - 1) { + if (name[i] == nameCapitalized[i]) { + url += "-" + } + url += name[i] + } + + return url.toLowerCase() + } } diff --git a/src/main/kotlin/app/Logger.kt b/src/main/kotlin/app/Logger.kt index 0e31cfca..373ca9e1 100644 --- a/src/main/kotlin/app/Logger.kt +++ b/src/main/kotlin/app/Logger.kt @@ -46,12 +46,12 @@ object Logger { /** * Log error message with exception info. */ - fun error(message: String, e: Exception) { + fun error(message: String, e: Throwable) { if (LEVEL >= ERROR) { println("[e] $message: $e") } - Analytics.trackError() + Analytics.trackError(e) } /** From ba019bd28250df1b46b1e0769bd975332e3a5414 Mon Sep 17 00:00:00 2001 From: Anatoly Kislov Date: Mon, 2 Oct 2017 14:26:10 +0300 Subject: [PATCH 4/4] feat: uncaught excetion handle, logs for analytics errors --- src/main/kotlin/app/Analytics.kt | 7 +++--- src/main/kotlin/app/Logger.kt | 24 +++++++++------------ src/main/kotlin/app/Main.kt | 14 +++++++++--- src/main/kotlin/app/api/ServerApi.kt | 8 +++---- src/main/kotlin/app/hashers/CommitHasher.kt | 2 +- src/main/kotlin/app/utils/RepoHelper.kt | 3 +-- 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/app/Analytics.kt b/src/main/kotlin/app/Analytics.kt index 988be861..ecbe740e 100644 --- a/src/main/kotlin/app/Analytics.kt +++ b/src/main/kotlin/app/Analytics.kt @@ -75,7 +75,7 @@ object Analytics { post(params + defaultParams.filter { !params.contains(it) } + idParams).responseString() } catch (e: Throwable) { - // Never fail from analytics. + Logger.error("Error while sending error report", e, logOnly = true) } } @@ -103,9 +103,8 @@ object Analytics { trackEvent("hashing/success") } - fun trackError(e: Throwable = Throwable()) { - // TODO(anatoly): Send message with exd, consider limit 150 bytes. - val url = getErrorUrl(e) + fun trackError(e: Throwable? = null, code: String = "") { + val url = if (e != null) getErrorUrl(e) else code val separator = if (url.isNotEmpty()) "/" else "" trackEvent("error" + separator + url, listOf("t" to HIT_EXCEPTION)) } diff --git a/src/main/kotlin/app/Logger.kt b/src/main/kotlin/app/Logger.kt index 373ca9e1..0b2d913f 100644 --- a/src/main/kotlin/app/Logger.kt +++ b/src/main/kotlin/app/Logger.kt @@ -34,24 +34,20 @@ object Logger { /** * Log error message with exception info. + * + * @property message the message for user and logs. + * @property e the exception if presented. + * @property code the code of error if exception is not presented. */ - fun error(message: String) { + fun error(message: String, e: Throwable? = null, code: String = "", + logOnly: Boolean = false) { if (LEVEL >= ERROR) { - println("[e] $message.") + println("[e] $message" + if (e != null) ": $e" else "") } - - Analytics.trackError() // May be not clear, but affects all. - } - - /** - * Log error message with exception info. - */ - fun error(message: String, e: Throwable) { - if (LEVEL >= ERROR) { - println("[e] $message: $e") + if (!logOnly) { + Analytics.trackError(e = e, code = code) + //TODO(anatoly): Add error tracking software. } - - Analytics.trackError(e) } /** diff --git a/src/main/kotlin/app/Main.kt b/src/main/kotlin/app/Main.kt index 9c31b435..72be7309 100644 --- a/src/main/kotlin/app/Main.kt +++ b/src/main/kotlin/app/Main.kt @@ -19,6 +19,9 @@ import com.beust.jcommander.JCommander import com.beust.jcommander.MissingCommandException fun main(argv : Array) { + Thread.setDefaultUncaughtExceptionHandler { _, e: Throwable? -> + Logger.error("Uncaught exception", e) + } Main(argv) } @@ -61,7 +64,10 @@ class Main(argv: Array) { else -> startUi() } } catch (e: MissingCommandException) { - Logger.error("No such command: ${e.unknownCommand}") + Logger.error( + message = "No such command: ${e.unknownCommand}", + code = "no-command" + ) } Analytics.trackExit() @@ -82,7 +88,8 @@ class Main(argv: Array) { Analytics.trackConfigChanged() } else { - Logger.error("No valid git repository found at $path.") + Logger.error(message = "No valid git repository found at $path.", + code = "repo-invalid") } } @@ -90,7 +97,8 @@ class Main(argv: Array) { val (key, value) = commandOptions.pair if (!arrayListOf("username", "password").contains(key)) { - Logger.error("No such key $key") + Logger.error(message = "No such key $key", + code = "invalid-params") return } diff --git a/src/main/kotlin/app/api/ServerApi.kt b/src/main/kotlin/app/api/ServerApi.kt index f0899705..58312f1d 100644 --- a/src/main/kotlin/app/api/ServerApi.kt +++ b/src/main/kotlin/app/api/ServerApi.kt @@ -113,13 +113,13 @@ class ServerApi (private val configurator: Configurator) : Api { try { Logger.debug("Request $requestName initialized") val (_, res, result) = request.responseString() - val (_, error) = result - if (error == null) { + val (_, e) = result + if (e == null) { Logger.debug("Request $requestName success") return parser(res.data) } else { - Logger.error("Request $requestName error", error) - throw RequestException(error) + Logger.error("Request $requestName error", e) + throw RequestException(e) } } catch (e: InvalidProtocolBufferException) { Logger.error("Request $requestName error while parsing", e) diff --git a/src/main/kotlin/app/hashers/CommitHasher.kt b/src/main/kotlin/app/hashers/CommitHasher.kt index e9cc1d81..0c514044 100644 --- a/src/main/kotlin/app/hashers/CommitHasher.kt +++ b/src/main/kotlin/app/hashers/CommitHasher.kt @@ -95,7 +95,7 @@ class CommitHasher(private val localRepo: LocalRepo, .subscribe({ commitsBundle -> // OnNext. postCommitsToServer(commitsBundle) // Send ready commits. }, { e -> // OnError. - Logger.error("Hashing error: " + e.message) + Logger.error("Hashing error", e) throwables.add(e) // TODO(anatoly): Top-class handling errors. }) } diff --git a/src/main/kotlin/app/utils/RepoHelper.kt b/src/main/kotlin/app/utils/RepoHelper.kt index ff87f00b..2cb5f3de 100644 --- a/src/main/kotlin/app/utils/RepoHelper.kt +++ b/src/main/kotlin/app/utils/RepoHelper.kt @@ -32,7 +32,7 @@ object RepoHelper { repository = git.repository commitId = repository.resolve(MASTER_BRANCH) } catch (e: Exception) { - Logger.error("Cannot access repository at path $path", e) + Logger.error("Cannot access repository at path $path", e) return false } finally { repository?.close() @@ -42,7 +42,6 @@ object RepoHelper { if (commitId != null) { return true } - Logger.error("Repository at path $path is empty") return false }