Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ App Sizer provides two flexible integration methods:

## Report Types

App Sizer currently supports three types of reports:
App Sizer currently supports four types of reports:

* HTML dashboard - a single self-contained file per device for interactive local analysis, no server required.
* InfluxDB database (1.x) - It is suitable for CI tracking and enabling the creation of customized dashboards (with visualization tools like Grafana). We provide an InfluxDB and Grafana setup; see our [Docker Setup Guide][grafana_docker_doc].
* Markdown table for convenient local analysis.
* JSON data for compatibility with other platforms.
Expand Down
11 changes: 11 additions & 0 deletions app-sizer/src/main/kotlin/com/grab/sizer/AppSizer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import com.grab.sizer.di.AnalyzerComponent
import com.grab.sizer.utils.InputProvider
import com.grab.sizer.utils.Logger
import com.grab.sizer.utils.OutputProvider
import com.grab.sizer.utils.log
import java.io.File

class AppSizer(
private val inputProvider: InputProvider,
Expand Down Expand Up @@ -67,5 +69,14 @@ class AppSizer(
}
}
}
logReportLocation()
}

private fun logReportLocation() {
val reportDirectory = File(
outputProvider.provideOutPutDirectory(),
outputProvider.provideProjectInfo().deviceName
)
logger.log("Reports generated at ${reportDirectory.toPath().toUri()}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import com.grab.sizer.report.DatabaseReportWriter
import com.grab.sizer.report.MarkdownReportWriter
import com.grab.sizer.report.ReportWriter
import com.grab.sizer.report.db.DbReportDaoFactory
import com.grab.sizer.report.html.HtmlReportWriter
import com.grab.sizer.report.json.JsonReportWriter
import com.grab.sizer.utils.InputProvider
import com.grab.sizer.utils.Logger
Expand Down Expand Up @@ -154,6 +155,11 @@ class AnalyzerComponent(
projectInfo = outputProvider.provideProjectInfo(),
customProperties = outputProvider.provideCustomProperties(),
),
HtmlReportWriter(
outputDirectory = outputProvider.provideOutPutDirectory(),
projectInfo = outputProvider.provideProjectInfo(),
customProperties = outputProvider.provideCustomProperties(),
),
DatabaseReportWriter(
reportDaoSet = lazy { DbReportDaoFactory(outputProvider, logger).create() },
projectInfo = outputProvider.provideProjectInfo(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* MIT License
*
* Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved.
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE
*/

package com.grab.sizer.report.html

import com.google.gson.Gson
import com.grab.sizer.report.CustomProperties
import com.grab.sizer.report.FIELD_KEY_CONTRIBUTOR
import com.grab.sizer.report.FIELD_KEY_OWNER
import com.grab.sizer.report.FIELD_KEY_SIZE
import com.grab.sizer.report.FIELD_KEY_TAG
import com.grab.sizer.report.ProjectInfo
import com.grab.sizer.report.Report
import com.grab.sizer.report.ReportWriter
import com.grab.sizer.report.Row
import java.io.File

private const val TEMPLATE_RESOURCE = "/com/grab/sizer/report/html/dashboard-template.html"
private const val DATA_PLACEHOLDER = "__DASHBOARD_DATA__"
internal const val DASHBOARD_FILE_NAME = "index.html"

/**
* Writes all reports of an analysis run into a single self-contained `index.html`
* dashboard, placed next to the markdown and JSON reports of the device.
*
* The dashboard embeds the report data as JSON inside an HTML template; charts are
* rendered client side with inline SVG. The file has no external dependencies (no CDN
* scripts, fonts, or network calls), so it can be opened offline or from a CI artifact
* browser.
*
* [write] is called once per report; the dashboard is regenerated on every call with
* all reports accumulated so far, keeping the [ReportWriter] contract unchanged. The
* last write therefore produces the complete dashboard.
*/
class HtmlReportWriter(
private val outputDirectory: File,
private val projectInfo: ProjectInfo,
private val customProperties: CustomProperties,
private val gson: Gson = Gson(),
) : ReportWriter {
private val reports = LinkedHashMap<String, Report>()

private val template: String by lazy {
javaClass.getResourceAsStream(TEMPLATE_RESOURCE)
?.bufferedReader()
?.use { it.readText() }
?: throw IllegalStateException("Dashboard template not found: $TEMPLATE_RESOURCE")
}

override fun write(report: Report) {
reports[report.id] = report
val dashboard = File(File(outputDirectory, projectInfo.deviceName), DASHBOARD_FILE_NAME)
dashboard.parentFile?.mkdirs()
dashboard.writeText(render())
}

private fun render(): String =
template.replace(DATA_PLACEHOLDER, gson.toJson(toDashboardData()))

private fun toDashboardData(): Map<String, Any> = mapOf(
"project" to mapOf(
"name" to projectInfo.projectName,
"version" to projectInfo.versionName,
"variant" to projectInfo.buildType,
"device" to projectInfo.deviceName,
"generatedAt" to System.currentTimeMillis(),
"customProperties" to customProperties,
),
"reports" to reports.mapValues { (_, report) -> report.rows.map { it.toEntry() } },
)

private fun Row.toEntry(): Map<String, Any> = buildMap {
put("name", stringField(FIELD_KEY_CONTRIBUTOR) ?: name)
put("size", (fields.find { it.name == FIELD_KEY_SIZE }?.value as? Number)?.toLong() ?: 0L)
stringField(FIELD_KEY_OWNER)?.let { put("owner", it) }
stringField(FIELD_KEY_TAG)?.let { put("tag", it) }
}

private fun Row.stringField(fieldName: String): String? =
fields.find { it.name == fieldName }?.value?.toString()
}
Loading
Loading