diff --git a/.github/ISSUE_TEMPLATE/desarrollo-de-un-indicatorstrategy.md b/.github/ISSUE_TEMPLATE/desarrollo-de-un-indicatorstrategy.md new file mode 100644 index 00000000..21ec2325 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/desarrollo-de-un-indicatorstrategy.md @@ -0,0 +1,10 @@ +--- +name: Desarrollo de un IndicatorStrategy +about: Desarrollo de una clase para el cálculo de un indicador +title: "[COD IndicatorStrategy]" +labels: '' +assignees: '' + +--- + +La clase desarrollada debe implementar la interfaz IndicatorStrategy para calcular un indicador a partir de unas métricas definidas. diff --git a/.github/ISSUE_TEMPLATE/desarrollo-de-un-nuevo-remoteenquirer.md b/.github/ISSUE_TEMPLATE/desarrollo-de-un-nuevo-remoteenquirer.md new file mode 100644 index 00000000..37960535 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/desarrollo-de-un-nuevo-remoteenquirer.md @@ -0,0 +1,11 @@ +--- +name: Desarrollo de un nuevo RemoteEnquirer +about: Desarrollo de una clase para consultar las métricas necesarias para el cálculo + de un indicador +title: "[COD RemoteEnquirer]" +labels: '' +assignees: '' + +--- + +Se solicita el desarrollo de una clase que implemente la interfaz RemoteEnquirer para consultar las métricas necesarias para el desarrollo del indicador asignado diff --git "a/.github/ISSUE_TEMPLATE/verificaci\303\263n-de-un-indicatorstrategy.md" "b/.github/ISSUE_TEMPLATE/verificaci\303\263n-de-un-indicatorstrategy.md" new file mode 100644 index 00000000..8e3eaefc --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/verificaci\303\263n-de-un-indicatorstrategy.md" @@ -0,0 +1,10 @@ +--- +name: Verificación de un IndicatorStrategy +about: Desarrollo de los tests para la verificación del IndicatorStrategy asignado +title: "[TEST IndicatorStrategy]" +labels: '' +assignees: '' + +--- + +Se deben desarrollar los test unidad para la verificación automática del IndicatorStrategy asignado en el issue: diff --git "a/.github/ISSUE_TEMPLATE/verificaci\303\263n-de-un-remoteenquirer.md" "b/.github/ISSUE_TEMPLATE/verificaci\303\263n-de-un-remoteenquirer.md" new file mode 100644 index 00000000..24622df4 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/verificaci\303\263n-de-un-remoteenquirer.md" @@ -0,0 +1,10 @@ +--- +name: Verificación de un RemoteEnquirer +about: Desarrollo de los tests para la verificación del RemoteEnquirer asignado +title: "[TEST RemoteEnquirer]" +labels: '' +assignees: '' + +--- + +Se deben desarrollar los test unidad para la verificación automática del RemoteEnquirer asignado en el issue: diff --git a/build.gradle b/build.gradle index 95067239..36f53c97 100644 --- a/build.gradle +++ b/build.gradle @@ -11,12 +11,19 @@ plugins { // Apply the java-library plugin for API and implementation separation. id 'java-library' +<<<<<<< HEAD + //A�ado el plugin para eclipse + id 'eclipse' + //para poder publicar paquetes en github + id 'maven-publish' +======= //Añado el plugin para eclipse id 'eclipse' //para poder publicar paquetes en github id 'maven-publish' //Plugin para análisis estático de código //id "nebula.lint" version "17.7.0" +>>>>>>> TAR_IndicatorStrategy } @@ -51,7 +58,11 @@ publishing { version = '0.2' //group = 'us.mitfs.samples' tasks.withType(JavaCompile) { +<<<<<<< HEAD + //A�adir la opci�n Xlint +======= //Añadir la opción Xlint +>>>>>>> TAR_IndicatorStrategy options.deprecation = true // options.encoding = 'ISO-8859-1' options.encoding = 'UTF-8' @@ -59,6 +70,12 @@ tasks.withType(JavaCompile) { tasks.withType(Javadoc){ +<<<<<<< HEAD + description = "Genera la documentaci�n" + //indicar que la codificaci�n es ISO + options.encoding = 'ISO-8859-1' + options.charSet = 'ISO-8859-1' +======= description = "Genera la documentación" //indicar que la codificación es ISO @@ -66,6 +83,7 @@ tasks.withType(Javadoc){ // options.charSet = 'ISO-8859-1' options.encoding = 'UTF-8' options.charSet = 'UTF-8' +>>>>>>> TAR_IndicatorStrategy options.author = true options.version = true options.use = true @@ -94,11 +112,24 @@ repositories { dependencies { // This dependency is exported to consumers, that is to say found on their compile classpath. api 'org.apache.commons:commons-math3:3.6.1' +<<<<<<< HEAD + //A�ado la dependencia de la librer�a github que vamos a usar + + // https://mvnrepository.com/artifact/org.kohsuke/github-api + //JAVADOC: https://github-api.kohsuke.org/apidocs/index.html + api 'org.kohsuke:github-api:1.301' + //Para la persistencia de informes usaremos la api apachepoi + // https://mvnrepository.com/artifact/org.apache.poi/poi + //JAVADOC: https://poi.apache.org/apidocs/5.0/ + implementation 'org.apache.poi:poi:5.2.1' + //Para leer la configuraci�n como ficheros con datos en formato json +======= //Añado la dependencia de la librería github que vamos a usar // https://mvnrepository.com/artifact/org.kohsuke/github-api //JAVADOC: https://github-api.kohsuke.org/apidocs/index.html api 'org.kohsuke:github-api:1.301' + //Para la persistencia de informes usaremos la api apachepoi // https://mvnrepository.com/artifact/org.apache.poi/poi //JAVADOC: https://poi.apache.org/apidocs/5.0/ @@ -106,6 +137,7 @@ dependencies { // https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.5' //Para leer la configuración como ficheros con datos en formato json +>>>>>>> TAR_IndicatorStrategy // https://mvnrepository.com/artifact/javax.json/javax.json-api //JAVADOC: https://javadoc.io/doc/org.glassfish/javax.json/latest/overview-summary.html implementation group: 'javax.json', name: 'javax.json-api', version: '1.1.4' @@ -117,17 +149,27 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:30.1.1-jre' +<<<<<<< HEAD + //A�ado para usar mockito +======= //Añado para usar mockito +>>>>>>> TAR_IndicatorStrategy //JAVADOC: https://javadoc.io/doc/org.mockito/mockito-core/4.3.1/overview-summary.html testImplementation 'org.mockito:mockito-core:4.3.1' // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter //JAVADOC: https://javadoc.io/doc/org.mockito/mockito-junit-jupiter/latest/index.html testImplementation 'org.mockito:mockito-junit-jupiter:4.3.1' +<<<<<<< HEAD + testImplementation(platform('org.junit:junit-bom:5.8.2')) + //JAVADOC: https://www.javadoc.io/doc/org.junit.jupiter/junit-jupiter-api/latest/index.html + testImplementation('org.junit.jupiter:junit-jupiter') +======= testImplementation(platform('org.junit:junit-bom:5.8.2')) //JAVADOC: https://www.javadoc.io/doc/org.junit.jupiter/junit-jupiter-api/latest/index.html testImplementation('org.junit.jupiter:junit-jupiter') testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +>>>>>>> TAR_IndicatorStrategy } @@ -139,6 +181,37 @@ test { } +<<<<<<< HEAD +//Para publicar paquetes en github +//group = 'A4I' +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/mit-fs/audit4improve-api") + credentials { + //las propiedades gpr.user y gpr.key est�n configuradas en gradle.properties en el raiz del proyecto, y se a�ade a .gitignore para que no se suban + //O bien configuro las variables de entorno GITHUB_LOGIN y GITHUB_PACKAGES + username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_LOGIN") + password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_PACKAGES") + } + } + } + publications { + gpr(MavenPublication) { + //Del tutorial https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven + + groupId = 'us.mitfs.samples' + artifactId = 'a4i' + version = '0.0' + + from components.java + } + + } +} +======= +>>>>>>> TAR_IndicatorStrategy diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/TARIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/TARIndicatorStrategy.java new file mode 100644 index 00000000..8941ee0b --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/TARIndicatorStrategy.java @@ -0,0 +1,70 @@ +package us.muit.fs.a4i.control.strategies; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.Collection; + +/** + * Estrategia para calcular el indicador de tráfico de acceso a repositorios de GitHub. + */ +public class TARIndicatorStrategy implements IndicatorStrategy { + private static final Logger log = Logger.getLogger(TARIndicatorStrategy.class.getName()); + + private static final List REQUIRED = List.of( + "uniqueVisitors", "uniqueClones" + ); + + @Override + public ReportItemI calcIndicator(List> metrics) + throws NotAvailableMetricException { + log.info("Calculando indicador de tráfico GitHub con métricas: " + metrics); + Map m = metrics.stream() + .collect(Collectors.toMap(ReportItemI::getName, ReportItemI::getValue)); + + for (String req : REQUIRED) { + if (!m.containsKey(req)) { + throw new NotAvailableMetricException("Falta métrica " + req); + } + } + + double visitors = m.get("uniqueVisitors"); + double clones = m.get("uniqueClones"); + + if (visitors <= 0) { + throw new NotAvailableMetricException("Número de visitantes debe ser mayor que cero."); + } + + double conversionRate = clones / visitors; + + IndicatorState state = classify(visitors); + + try { + return new ReportItem.ReportItemBuilder("GithubTraffic", conversionRate) + .metrics((Collection) metrics) + .indicator(state) + .build(); + } catch (ReportItemException e) { + throw new NotAvailableMetricException("Error construyendo ReportItem: " + e.getMessage()); + } + } + + @Override + public List requiredMetrics() { + return REQUIRED; + } + + private IndicatorState classify(double visitors) { + if (visitors < 10) return IndicatorState.CRITICAL; + if (visitors < 50) return IndicatorState.WARNING; + return IndicatorState.OK; + } +} diff --git a/src/main/java/us/muit/fs/a4i/model/remote/TARRemoteEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/TARRemoteEnquirer.java new file mode 100644 index 00000000..6c636f76 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/remote/TARRemoteEnquirer.java @@ -0,0 +1,134 @@ +package us.muit.fs.a4i.model.remote; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Logger; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; + +public class TARRemoteEnquirer extends GitHubEnquirer { + + private static final Logger log = Logger.getLogger(TARRemoteEnquirer.class.getName()); + private static final String GITHUB_API_BASE = "https://api.github.com"; + + public TARRemoteEnquirer() { + super(); + } + + @Override + public ReportItem getMetric(String metricName, String repoFullName) throws MetricException { + if ("uniqueVisitorsLastDay".equals(metricName)) { + return getUniqueVisitors(repoFullName); + } else if ("uniqueClonesLastDay".equals(metricName)) { + return getUniqueClones(repoFullName); + } else if ("cloneConversionRate".equals(metricName)) { + return getCloneConversionRate(repoFullName); + } else { + throw new MetricException("Métrica desconocida: " + metricName); + } + } + + private ReportItem getUniqueVisitors(String repoFullName) { + try { + String json = callGitHubAPI("/repos/" + repoFullName + "/traffic/visitors"); + int uniques = extractIntFromJson(json, "\"uniques\":"); + ReportItem.ReportItemBuilder builder = new ReportItem.ReportItemBuilder("uniqueVisitorsLastDay", Integer.valueOf(uniques)); + builder.source("GitHub REST API"); + builder.unit("Visitantes/día"); + return builder.build(); + } catch (Exception e) { + log.severe("Error obteniendo visitantes: " + e.getMessage()); + throw new MetricException("No se pudo obtener visitantes únicos."); + } + } + + private ReportItem getUniqueClones(String repoFullName) { + try { + String json = callGitHubAPI("/repos/" + repoFullName + "/traffic/clones"); + int uniques = extractIntFromJson(json, "\"uniques\":"); + ReportItem.ReportItemBuilder builder = new ReportItem.ReportItemBuilder("uniqueClonesLastDay", Integer.valueOf(uniques)); + builder.source("GitHub REST API"); + builder.unit("Clones/día"); + return builder.build(); + } catch (Exception e) { + log.severe("Error obteniendo clones: " + e.getMessage()); + throw new MetricException("No se pudo obtener clones únicos."); + } + } + + private ReportItem getCloneConversionRate(String repoFullName) { + try { + int visitors = ((Integer) getUniqueVisitors(repoFullName).getValue()).intValue(); + int clones = ((Integer) getUniqueClones(repoFullName).getValue()).intValue(); + double rate = (visitors > 0) ? ((double) clones / visitors) * 100.0 : 0.0; + + ReportItem.ReportItemBuilder builder = new ReportItem.ReportItemBuilder("cloneConversionRate", Double.valueOf(rate)); + builder.source("GitHub REST API"); + builder.unit("%"); + return builder.build(); + } catch (Exception e) { + throw new MetricException("No se pudo calcular la tasa de conversión."); + } + } + + private String callGitHubAPI(String endpoint) throws Exception { + String token = System.getProperty("github.token"); + if (token == null || token.isEmpty()) { + throw new MetricException("Token de GitHub no encontrado. Asegúrate de pasarlo con -Dgithub.token=..."); + } + + URL url = new URL(GITHUB_API_BASE + endpoint); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + + con.setRequestMethod("GET"); + con.setRequestProperty("Authorization", "token " + token); + con.setRequestProperty("Accept", "application/vnd.github.v3+json"); + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + + int status = con.getResponseCode(); + if (status != 200) { + throw new MetricException("Error en la conexión con GitHub. Código: " + status); + } + + InputStream in = con.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); + StringBuilder response = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + response.append(line); + } + + reader.close(); + con.disconnect(); + + return response.toString(); + } + + private int extractIntFromJson(String json, String key) { + int index = json.indexOf(key); + if (index == -1) return 0; + int start = index + key.length(); + int end = json.indexOf(",", start); + if (end == -1) end = json.indexOf("}", start); + String number = json.substring(start, end).trim(); + return Integer.parseInt(number); + } + + @Override + public ReportI buildReport(String entityId) { + return null; // opcional + } + + @Override + public RemoteType getRemoteType() { + return RemoteType.GITHUB; + } +} diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 33cee922..d1a56520 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -1,315 +1,342 @@ { - "metrics": [ - { - "name": "subscribers", - "type": "java.lang.Integer", - "description": "Número de suscriptores de un repositorio, watchers en la web", - "unit": "subscribers" - }, - { - "name": "collaborators", - "type": "java.lang.Integer", - "description": "Número de colaboradores de un repositorio", - "unit": "collaborators" - }, - { - "name": "ownerCommits", - "type": "java.lang.Integer", - "description": "Número de commits del propietario del repositorio", - "unit": "commits" - }, - { - "name": "members", - "type": "java.lang.Integer", - "description": "Miembros de una organización", - "unit": "members" - }, - { - "name": "teams", - "type": "java.lang.Integer", - "description": "Equipos de una organización", - "unit": "teams" - }, - { - "name": "forks", - "type": "java.lang.Integer", - "description": "Número de forks, no son los forks de la web", - "unit": "forks" - }, - { - "name": "watchers", - "type": "java.lang.Integer", - "description": "Observadores de un repositorio, en la web aparece com forks", - "unit": "watchers" - }, - { - "name": "followers", - "type": "java.lang.Integer", - "description": "Seguidores de una organización", - "unit": "followers" - }, - { - "name": "totalAdditions", - "type": "java.lang.Integer", - "description": "Inserciones desde que se inició el repositorio", - "unit": "additions" - }, - { - "name": "totalDeletions", - "type": "java.lang.Integer", - "description": "Eliminaciones desde que se inició el repositorio", - "unit": "deletions" - }, - { - "name": "stars", - "type": "java.lang.Integer", - "description": "Estrellas concedidas", - "unit": "stars" - }, - { - "name": "creation", - "type": "java.util.Date", - "description": "Fecha de creación del calendario", - "unit": "date" - }, - { - "name": "lastPush", - "type": "java.util.Date", - "description": "Último push realizado en el repositorio", - "unit": "date" - }, - { - "name": "lastUpdated", - "type": "java.util.Date", - "description": "Última actualización realizada en el repositorio", - "unit": "date" - }, - { - "name": "openIssues", - "type": "java.lang.Integer", - "description": "Numero de issues abiertas", - "unit": "issues" - }, - { - "name": "openProjects", - "type": "java.lang.Integer", - "description": "Proyectos abiertos", - "unit": "projects" - }, - { - "name": "closedProjects", - "type": "java.lang.Integer", - "description": "Proyectos cerrados", - "unit": "projects" - }, - { - "name": "repositories", - "type": "java.lang.Integer", - "description": "Número de repositorios", - "unit": "repositories" - }, - { - "name": "repositoriesWithPullRequest", - "type": "java.lang.Integer", - "description": "Número de repositorios con pull requests", - "unit": "repositories" - }, - { - "name": "repositoriesWithOpenPullRequest", - "type": "java.lang.Integer", - "description": "Número de repositorios con pull requests pendientes", - "unit": "repositories" - }, - { - "name": "closedIssues", - "type": "java.lang.Integer", - "description": "Numero de issues cerrados", - "unit": "issues" - }, - { - "name": "issues", - "type": "java.lang.Integer", - "description": "Tareas totales", - "unit": "issues" - }, - { - "name": "issuesLastMonth", - "type": "java.lang.Integer", - "description": "Número de trabajos/issues creados en el mes", - "unit": "issues/month" - }, - { - "name": "closedIssuesLastMonth", - "type": "java.lang.Integer", - "description": "Número de repositorios trabajos/issues concluidos en el mes", - "unit": "issues/month" - }, - { - "name": "meanClosedIssuesLastMonth", - "type": "java.lang.Double", - "description": "Media de issues cerrados en el pasado mes por desarrollador", - "unit": "issues/developer" - }, - { - "name": "assignedIssuesLastMonth", - "type": "java.lang.Integer", - "description": "Issues asignados a un desarrollador en el último mes", - "unit": "issues/month" - }, - { - "name": "pullRequests", - "type": "java.lang.Integer", - "description": "Número de pull requests", - "unit": "pull requests" - }, - { - "name": "totalPullReq", - "type": "java.lang.Integer", - "description": "Número de pull requests en un repositorio", - "unit": "PR" - }, - { - "name": "closedPullReq", - "type": "java.lang.Integer", - "description": "Número de pull requests cerradas en un repositorio", - "unit": "PR" - }, - { - "name": "PRAcceptedLastYear", - "type": "java.lang.Integer", - "description": "Número de pull requests aceptadas el pasado año", - "unit": "PR" - }, - { - "name": "PRAcceptedLastMonth", - "type": "java.lang.Integer", - "description": "Número de pull requests aceptadas el pasado mes", - "unit": "PR" - }, - { - "name": "PRRejectedLastMonth", - "type": "java.lang.Integer", - "description": "Número de pull requests rechazadas el pasado mes", - "unit": "PR" - }, - { - "name": "PRRejectedLastYear", - "type": "java.lang.Integer", - "description": "Número de pull requests rechazadas el pasado año", - "unit": "PR" - }, - { - "name": "conventionalCommits", - "type": "java.lang.Double", - "description": "Ratio de commits convencionales", - "unit": "ratio" - }, - { - "name": "commitsWithDescription", - "type": "java.lang.Double", - "description": "Ratio de commits con descripción", - "unit": "ratio" - }, - { - "name": "issuesWithLabels", - "type": "java.lang.Double", - "description": "Ratio de issues con más de una etiqueta", - "unit": "ratio" - }, - { - "name": "gitFlowBranches", - "type": "java.lang.Double", - "description": "Ratio de ramas que siguen gitflow", - "unit": "ratio" - }, - { - "name": "conventionalPullRequests", - "type": "java.lang.Double", - "description": "Ratio de PRs convencionales", - "unit": "ratio" - }, - { - "name": "teamsBalance", - "type": "java.lang.Integer", - "description": "Balance de equipos y open issues", - "unit": "ratio" - } - ], - "indicators": [ - { - "name": "issuesProgress", - "type": "java.lang.Double", - "description": "Ratio de issues cerrados frente a totales", - "unit": "ratio", - "limits": { - "ok": 2, - "warning": 4, - "critical": 6 - } - }, - { - "name": "overdued", - "type": "java.lang.Double", - "description": "Ratio de issues vencidos frente a abiertos", - "unit": "ratio", - "limits": { - "ok": 5, - "warning": 9, - "critical": 12 - } - }, - { - "name": "issuesRatio", - "type": "java.lang.Double", - "description": "Ratio de issues abiertos frente a cerrados", - "unit": "ratio" - }, - { - "name": "pullRequestCompletion", - "type": "java.lang.Double", - "description": "% PR cerrados frente a totales", - "unit": "%", - "limits": { - "ok": 75, - "warning": 50, - "critical": 25 - } - }, - { - "name": "developerPerfomance", - "type": "java.lang.Double", - "description": "Ratio de issues cerradas frente a creadas del desarrollador frente al equipo", - "unit": "ratio" - }, - { - "name": "PRPerformance", - "type": "java.lang.Double", - "description": "% de PR rechazados en el mes comparado con el % rechazado en el año", - "unit": "%", - "limits": { - "ok": 25, - "warning": 50, - "critical": 75 - } - }, - { - "name": "conventionsCompliant", - "type": "java.lang.Double", - "description": "Indicador de conformidad con las convenciones en el repo", - "unit": "ratio" - }, - { - "name": "teamsBalanceI", - "type": "java.lang.Double", - "description": "Balance de equipos y open issues", - "unit": "ratio", - "limits": { "ok": 0.2, "warning": 0.6, "critical": 0.8 } - }, - { - "name": "fixTime", - "type": "java.lang.Double", - "description": "Tiempo para arreglos", - "unit": "ratio" - } - ] -} \ No newline at end of file + "metrics": [ + { + "name": "subscribers", + "type": "java.lang.Integer", + "description": "Número de suscriptores de un repositorio, watchers en la web", + "unit": "subscribers" + }, + { + "name": "collaborators", + "type": "java.lang.Integer", + "description": "Número de colaboradores de un repositorio", + "unit": "collaborators" + }, + { + "name": "ownerCommits", + "type": "java.lang.Integer", + "description": "Número de commits del propietario del repositorio", + "unit": "commits" + }, + { + "name": "members", + "type": "java.lang.Integer", + "description": "Miembros de una organización", + "unit": "members" + }, + { + "name": "teams", + "type": "java.lang.Integer", + "description": "Equipos de una organización", + "unit": "teams" + }, + { + "name": "forks", + "type": "java.lang.Integer", + "description": "Número de forks, no son los forks de la web", + "unit": "forks" + }, + { + "name": "watchers", + "type": "java.lang.Integer", + "description": "Observadores de un repositorio, en la web aparece com forks", + "unit": "watchers" + }, + { + "name": "followers", + "type": "java.lang.Integer", + "description": "Seguidores de una organización", + "unit": "followers" + }, + { + "name": "totalAdditions", + "type": "java.lang.Integer", + "description": "Inserciones desde que se inició el repositorio", + "unit": "additions" + }, + { + "name": "totalDeletions", + "type": "java.lang.Integer", + "description": "Eliminaciones desde que se inició el repositorio", + "unit": "deletions" + }, + { + "name": "stars", + "type": "java.lang.Integer", + "description": "Estrellas concedidas", + "unit": "stars" + }, + { + "name": "creation", + "type": "java.util.Date", + "description": "Fecha de creación del calendario", + "unit": "date" + }, + { + "name": "lastPush", + "type": "java.util.Date", + "description": "Último push realizado en el repositorio", + "unit": "date" + }, + { + "name": "lastUpdated", + "type": "java.util.Date", + "description": "Última actualización realizada en el repositorio", + "unit": "date" + }, + { + "name": "openIssues", + "type": "java.lang.Integer", + "description": "Numero de issues abiertas", + "unit": "issues" + }, + { + "name": "openProjects", + "type": "java.lang.Integer", + "description": "Proyectos abiertos", + "unit": "projects" + }, + { + "name": "closedProjects", + "type": "java.lang.Integer", + "description": "Proyectos cerrados", + "unit": "projects" + }, + { + "name": "repositories", + "type": "java.lang.Integer", + "description": "Número de repositorios", + "unit": "repositories" + }, + { + "name": "repositoriesWithPullRequest", + "type": "java.lang.Integer", + "description": "Número de repositorios con pull requests", + "unit": "repositories" + }, + { + "name": "repositoriesWithOpenPullRequest", + "type": "java.lang.Integer", + "description": "Número de repositorios con pull requests pendientes", + "unit": "repositories" + }, + { + "name": "closedIssues", + "type": "java.lang.Integer", + "description": "Numero de issues cerrados", + "unit": "issues" + }, + { + "name": "issues", + "type": "java.lang.Integer", + "description": "Tareas totales", + "unit": "issues" + }, + { + "name": "issuesLastMonth", + "type": "java.lang.Integer", + "description": "Número de trabajos/issues creados en el mes", + "unit": "issues/month" + }, + { + "name": "closedIssuesLastMonth", + "type": "java.lang.Integer", + "description": "Número de repositorios trabajos/issues concluidos en el mes", + "unit": "issues/month" + }, + { + "name": "meanClosedIssuesLastMonth", + "type": "java.lang.Double", + "description": "Media de issues cerrados en el pasado mes por desarrollador", + "unit": "issues/developer" + }, + { + "name": "assignedIssuesLastMonth", + "type": "java.lang.Integer", + "description": "Issues asignados a un desarrollador en el último mes", + "unit": "issues/month" + }, + { + "name": "pullRequests", + "type": "java.lang.Integer", + "description": "Número de pull requests", + "unit": "pull requests" + }, + { + "name": "totalPullReq", + "type": "java.lang.Integer", + "description": "Número de pull requests en un repositorio", + "unit": "PR" + }, + { + "name": "closedPullReq", + "type": "java.lang.Integer", + "description": "Número de pull requests cerradas en un repositorio", + "unit": "PR" + }, + { + "name": "PRAcceptedLastYear", + "type": "java.lang.Integer", + "description": "Número de pull requests aceptadas el pasado año", + "unit": "PR" + }, + { + "name": "PRAcceptedLastMonth", + "type": "java.lang.Integer", + "description": "Número de pull requests aceptadas el pasado mes", + "unit": "PR" + }, + { + "name": "PRRejectedLastMonth", + "type": "java.lang.Integer", + "description": "Número de pull requests rechazadas el pasado mes", + "unit": "PR" + }, + { + "name": "PRRejectedLastYear", + "type": "java.lang.Integer", + "description": "Número de pull requests rechazadas el pasado año", + "unit": "PR" + }, + { + "name": "conventionalCommits", + "type": "java.lang.Double", + "description": "Ratio de commits convencionales", + "unit": "ratio" + }, + { + "name": "commitsWithDescription", + "type": "java.lang.Double", + "description": "Ratio de commits con descripción", + "unit": "ratio" + }, + { + "name": "issuesWithLabels", + "type": "java.lang.Double", + "description": "Ratio de issues con más de una etiqueta", + "unit": "ratio" + }, + { + "name": "gitFlowBranches", + "type": "java.lang.Double", + "description": "Ratio de ramas que siguen gitflow", + "unit": "ratio" + }, + { + "name": "conventionalPullRequests", + "type": "java.lang.Double", + "description": "Ratio de PRs convencionales", + "unit": "ratio" + }, + { + "name": "teamsBalance", + "type": "java.lang.Integer", + "description": "Balance de equipos y open issues", + "unit": "ratio" + }, + { + "name": "uniqueVisitors", + "type": "java.lang.Double", + "description": "Número de visitantes únicos diarios al repositorio", + "unit": "visitors/day" + }, + { + "name": "uniqueClones", + "type": "java.lang.Double", + "description": "Número de clonaciones únicas diarias del repositorio", + "unit": "clones/day" + } + ], + "indicators": [ + { + "name": "issuesProgress", + "type": "java.lang.Double", + "description": "Ratio de issues cerrados frente a totales", + "unit": "ratio", + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } + }, + { + "name": "overdued", + "type": "java.lang.Double", + "description": "Ratio de issues vencidos frente a abiertos", + "unit": "ratio", + "limits": { + "ok": 5, + "warning": 9, + "critical": 12 + } + }, + { + "name": "issuesRatio", + "type": "java.lang.Double", + "description": "Ratio de issues abiertos frente a cerrados", + "unit": "ratio" + }, + { + "name": "pullRequestCompletion", + "type": "java.lang.Double", + "description": "% PR cerrados frente a totales", + "unit": "%", + "limits": { + "ok": 75, + "warning": 50, + "critical": 25 + } + }, + { + "name": "developerPerfomance", + "type": "java.lang.Double", + "description": "Ratio de issues cerradas frente a creadas del desarrollador frente al equipo", + "unit": "ratio" + }, + { + "name": "PRPerformance", + "type": "java.lang.Double", + "description": "% de PR rechazados en el mes comparado con el % rechazado en el año", + "unit": "%", + "limits": { + "ok": 25, + "warning": 50, + "critical": 75 + } + }, + { + "name": "conventionsCompliant", + "type": "java.lang.Double", + "description": "Indicador de conformidad con las convenciones en el repo", + "unit": "ratio" + }, + { + "name": "teamsBalanceI", + "type": "java.lang.Double", + "description": "Balance de equipos y open issues", + "unit": "ratio", + "limits": { + "ok": 0.2, + "warning": 0.6, + "critical": 0.8 + } + }, + { + "name": "fixTime", + "type": "java.lang.Double", + "description": "Tiempo para arreglos", + "unit": "ratio" + }, + { + "name": "GithubTraffic", + "type": "java.lang.Double", + "description": "Tasa de conversión de visitantes únicos a clonaciones únicas en un repositorio GitHub", + "unit": "conversion rate", + "limits": { + "ok": 50, + "warning": 10, + "critical": 0 + } + } + ] +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java index 11a43dd3..26032954 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java +++ b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java @@ -1,4 +1,9 @@ package us.muit.fs.a4i.test.control; +<<<<<<< HEAD +import org.kohsuke.github.*; +import org.kohsuke.github.GHRepositoryStatistics.ContributorStats; +======= +>>>>>>> TAR_IndicatorStrategy import java.util.HashMap; //import java.util.Iterator; @@ -27,11 +32,34 @@ public class SupervisorControl { private static Logger log = Logger.getLogger(SupervisorControl.class.getName()); /** +<<<<<<< HEAD + * @author Isabel Rom�n Mart�nez + * @version 0.0 + * Esta clase se crea para poder probar algunas de las capacidades que ofrece la api github + * Ser� descartada posteriormente + * No usa Junit, sino que crea un main, no tiene verificaciones autom�ticas, la automatizaci�n no es posible + * +======= * @param args +>>>>>>> TAR_IndicatorStrategy */ public static void main(String[] args) { try { GitHub github = GitHubBuilder.fromEnvironment().build(); +<<<<<<< HEAD + GHMyself myinfo=github.getMyself(); + //GHRepository unrepo=github.getRepository("MIT-FS/ShopManager"); + //PagedIterable myrepos=myinfo.listRepositories(); + PagedIterable myOwnRepos=myinfo.listRepositories(10, GHMyself.RepositoryListFilter.OWNER); + int count=1; + for(GHRepository repo:myOwnRepos.toList()) { + System.out.println("Nombre de mi repositorio n�mero "+count+" "+repo.getFullName()); + List proyectos=repo.listProjects().toList(); + int i=1; + for(GHProject project:proyectos){ + System.out.println("Con proyecto "+i+" llamado "+project.getName()+" con id "+project.getId()); + GHProject audit= github.getProject(project.getId()); +======= GHMyself myinfo = github.getMyself(); // GHRepository unrepo=github.getRepository("MIT-FS/ShopManager"); // PagedIterable myrepos=myinfo.listRepositories(); @@ -45,6 +73,7 @@ public static void main(String[] args) { System.out.println( "Con proyecto " + i + " llamado " + project.getName() + " con id " + project.getId()); GHProject audit = github.getProject(project.getId()); +>>>>>>> TAR_IndicatorStrategy System.out.println(audit); i++; } @@ -53,6 +82,57 @@ public static void main(String[] args) { } count++; } +<<<<<<< HEAD + GHPersonSet misOrganizaciones = myinfo.getAllOrganizations(); + System.out.println("Pertenezco a las siguientes organizaciones: "); + //Iterator iteradorOrganizaciones = misOrganizaciones.iterator(); + int i=1; + for(GHOrganization organizacion: misOrganizaciones) { + System.out.println(i+" Organizaci�n "+organizacion.getId()+" : "+organizacion); + PagedIterable repos=organizacion.listRepositories(); + System.out.println(repos); + i++; + } + /* + log.info("Mis datos "+myinfo); + log.info("Un repositorio "+unrepo); + log.info("N�mero de repositorios "+myrepos.toList().size()); + log.info("Detalles de mis repositorios "+myrepos.toList()); + */ + GHOrganization unaOrg = github.getOrganization("MIT-FS"); + // PagedIterable repos=unaOrg.listRepositories(); + System.out.println("Recupero la organizaci�n "+unaOrg.getId()); + GHRepository githubrepo=github.getRepository("MIT-FS/Audit4Improve-API"); + System.out.println("Este repositorio es de "+githubrepo.getOwnerName()+" Y su descripci�n es "+githubrepo.getDescription()); + GHRepositoryStatistics estadisticas=githubrepo.getStatistics(); + log.info("Estadisticas recogidas"); + + // List proyectos=githubrepo.listProjects().toList(); + PagedIterable estDes=estadisticas.getContributorStats(); + log.info("Desarrolladores recogidos"); + + List listaDesarrolladores=estDes.toList(); + + System.out.println("N�mero de desarrolladores "+listaDesarrolladores.size()); + + i=1; + + + HashMap mapaEstadisticasUsuario = new HashMap (); + for (GHRepositoryStatistics.ContributorStats desarrollador:listaDesarrolladores) { + System.out.println(i+" Desarrollador "+desarrollador.getAuthor().getName()+" mail "+desarrollador.getAuthor().getEmail()+ " login "+desarrollador.getAuthor().getLogin()); + GHUser usuario=github.getUser(desarrollador.getAuthor().getLogin()); + mapaEstadisticasUsuario.put(desarrollador.getAuthor().getLogin(), desarrollador); + i++; + } + System.out.println("Datos del usuario Isabel-Roman "+mapaEstadisticasUsuario.get("Isabel-Roman")); + System.out.println("Semanas "+mapaEstadisticasUsuario.get("Isabel-Roman").getWeeks()); + + /*for(GHProject project:proyectos){ + System.out.println("Con proyecto "+i+" llamado "+project.getName()+" con id "+project.getId()); + GHProject audit= github.getProject(project.getId()); + System.out.println(audit); +======= GHPersonSet misOrganizaciones = myinfo.getAllOrganizations(); System.out.println("Pertenezco a las siguientes organizaciones: "); // Iterator iteradorOrganizaciones = @@ -62,6 +142,7 @@ public static void main(String[] args) { System.out.println(i + " Organización " + organizacion.getId() + " : " + organizacion); PagedIterable repos = organizacion.listRepositories(); System.out.println(repos); +>>>>>>> TAR_IndicatorStrategy i++; } /* diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/TARIndicatorStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/TARIndicatorStrategyTest.java new file mode 100644 index 00000000..f184601e --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/TARIndicatorStrategyTest.java @@ -0,0 +1,104 @@ +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.control.strategies.TARIndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.entities.ReportItem; + +public class TARIndicatorStrategyTest { + private static Logger log = Logger.getLogger(TARIndicatorStrategyTest.class.getName()); + private TARIndicatorStrategy strat; + + @BeforeEach + void setUp() { + strat = new TARIndicatorStrategy(); + } + + @Test + public void testTAR_Alto() throws NotAvailableMetricException, ReportItemException { + ReportItemI visitors = mock(ReportItemI.class); + ReportItemI clones = mock(ReportItemI.class); + + when(visitors.getName()).thenReturn("uniqueVisitors"); + when(visitors.getValue()).thenReturn(60.0); + when(clones.getName()).thenReturn("uniqueClones"); + when(clones.getValue()).thenReturn(30.0); + + List> metrics = List.of(visitors, clones); + ReportItemI result = strat.calcIndicator(metrics); + + assertEquals("GithubTraffic", result.getName()); + assertEquals(0.5, result.getValue(), 1e-6); + assertEquals(IndicatorState.OK, result.getIndicator().getState()); + } + + @Test + public void testTAR_Medio() throws NotAvailableMetricException, ReportItemException { + ReportItemI visitors = mock(ReportItemI.class); + ReportItemI clones = mock(ReportItemI.class); + + when(visitors.getName()).thenReturn("uniqueVisitors"); + when(visitors.getValue()).thenReturn(30.0); + when(clones.getName()).thenReturn("uniqueClones"); + when(clones.getValue()).thenReturn(9.0); + + List> metrics = List.of(visitors, clones); + ReportItemI result = strat.calcIndicator(metrics); + + assertEquals("GithubTraffic", result.getName()); + assertEquals(0.3, result.getValue(), 1e-6); + assertEquals(IndicatorState.WARNING, result.getIndicator().getState()); + } + + @Test + public void testTAR_Bajo() throws NotAvailableMetricException, ReportItemException { + ReportItemI visitors = mock(ReportItemI.class); + ReportItemI clones = mock(ReportItemI.class); + + when(visitors.getName()).thenReturn("uniqueVisitors"); + when(visitors.getValue()).thenReturn(5.0); + when(clones.getName()).thenReturn("uniqueClones"); + when(clones.getValue()).thenReturn(1.0); + + List> metrics = List.of(visitors, clones); + ReportItemI result = strat.calcIndicator(metrics); + + assertEquals("GithubTraffic", result.getName()); + assertEquals(0.2, result.getValue(), 1e-6); + assertEquals(IndicatorState.CRITICAL, result.getIndicator().getState()); + } + + @Test + public void testTAR_VisitantesCero() { + ReportItemI visitors = mock(ReportItemI.class); + ReportItemI clones = mock(ReportItemI.class); + + when(visitors.getName()).thenReturn("uniqueVisitors"); + when(visitors.getValue()).thenReturn(0.0); + when(clones.getName()).thenReturn("uniqueClones"); + when(clones.getValue()).thenReturn(5.0); + + List> metrics = List.of(visitors, clones); + + assertThrows(NotAvailableMetricException.class, () -> strat.calcIndicator(metrics)); + } + + @Test + public void testTAR_FaltanMetricas() throws ReportItemException { + ReportItemI onlyVisitors = new ReportItem.ReportItemBuilder<>("uniqueVisitors", 25.0).build(); + List> metrics = List.of(onlyVisitors); + + assertThrows(NotAvailableMetricException.class, () -> strat.calcIndicator(metrics)); + } +} diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/TARRemoteEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/TARRemoteEnquirerTest.java new file mode 100644 index 00000000..81289de2 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/TARRemoteEnquirerTest.java @@ -0,0 +1,110 @@ +package us.muit.fs.a4i.test.model.remote; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.remote.TARRemoteEnquirer; + +/** + * Test de integración para TARRemoteEnquirer. + */ +class TARRemoteEnquirerTest { + + private static final Logger log = Logger.getLogger(TARRemoteEnquirerTest.class.getName()); + private static final String REPO = "MIT-FS/Audit4Improve-API-G10"; + + private TARRemoteEnquirer ghEnquirer; + + @BeforeEach + void setUp() { + String token = System.getProperty("github.token"); + assertNotNull(token, "Debe definir github.token como propiedad del sistema con -Dgithub.token=..."); + ghEnquirer = new TARRemoteEnquirer(); + } + + @Test + @DisplayName("uniqueVisitorsLastDay: total visitantes únicos") + void testUniqueVisitors() throws MetricException { + ReportItem item = (ReportItem) ghEnquirer.getMetric("uniqueVisitorsLastDay", REPO); + + assertTrue(item.getValue() instanceof Integer, "El valor debe ser Integer"); + + Integer value = (Integer) item.getValue(); + ReportItemI metric = (ReportItemI) item; + + assertEquals("uniqueVisitorsLastDay", metric.getName()); + log.info("uniqueVisitorsLastDay = " + value); + assertTrue(value >= 0); + } + + @Test + @DisplayName("uniqueClonesLastDay: total clones únicos") + void testUniqueClones() throws MetricException { + ReportItem item = (ReportItem) ghEnquirer.getMetric("uniqueClonesLastDay", REPO); + + assertTrue(item.getValue() instanceof Integer, "El valor debe ser Integer"); + + Integer value = (Integer) item.getValue(); + ReportItemI metric = (ReportItemI) item; + + assertEquals("uniqueClonesLastDay", metric.getName()); + log.info("uniqueClonesLastDay = " + value); + assertTrue(value >= 0); + } + + @Test + @DisplayName("cloneConversionRate: conversión (%) de visitas a clones") + void testCloneConversionRate() throws MetricException { + ReportItem item = (ReportItem) ghEnquirer.getMetric("cloneConversionRate", REPO); + + assertTrue(item.getValue() instanceof Double, "El valor debe ser Double"); + + Double value = (Double) item.getValue(); + ReportItemI metric = (ReportItemI) item; + + assertEquals("cloneConversionRate", metric.getName()); + log.info("cloneConversionRate = " + value + "%"); + assertTrue(value >= 0.0); + } + + @Test + @DisplayName("getAvailableMetrics() debe listar las 3 métricas") + void testGetAvailableMetrics() { + List list = ghEnquirer.getAvailableMetrics(); + log.info("Available metrics: " + list); + assertEquals(3, list.size()); + assertTrue(list.contains("uniqueVisitorsLastDay")); + assertTrue(list.contains("uniqueClonesLastDay")); + assertTrue(list.contains("cloneConversionRate")); + } + + @Test + @DisplayName("buildReport() debe devolver ReportI con 3 ítems") + void testBuildReport() { + ReportI report = ghEnquirer.buildReport(REPO); + assertNotNull(report, "El reporte no debe ser nulo"); + + List> items = new ArrayList>(); + for (ReportItemI metric : report.getAllMetrics()) { + items.add(metric); + } + + log.info("Informe generado con ítems: " + items); + assertEquals(3, items.size(), "Informe debe contener 3 métricas"); + + assertTrue(items.stream().anyMatch(i -> "uniqueVisitorsLastDay".equals(i.getName()))); + assertTrue(items.stream().anyMatch(i -> "uniqueClonesLastDay".equals(i.getName()))); + assertTrue(items.stream().anyMatch(i -> "cloneConversionRate".equals(i.getName()))); + } +} diff --git a/src/test/resources/appConfTest.json b/src/test/resources/appConfTest.json index ae5c655c..ebd73ec2 100644 --- a/src/test/resources/appConfTest.json +++ b/src/test/resources/appConfTest.json @@ -1,40 +1,63 @@ { - "metrics": [ - { - "name": "downloads", - "type": "java.lang.Integer", - "description": "Descargas realizadas", - "unit": "downloads" - }, - { - "name": "comments", - "type": "java.lang.Integer", - "description": "Número de comentarios", - "unit": "comments" - } - ], - "indicators": [ - { - "name": "pullReqGlory", - "type": "java.lang.Double", - "description": "Ratio de pull request aceptados frente a solicitados", - "unit": "ratio", - "limits": { - "ok": 2, - "warning": 4, - "critical": 6 - } - }, - { - "name": "commentsInterest", - "type": "java.lang.Double", - "description": "Ratio de comentarios con más de 1 respuesta frente al número total de comentarios", - "unit": "ratio", - "limits": { - "ok": 2, - "warning": 4, - "critical": 6 - } - } - ] -} \ No newline at end of file + "metrics": [ + { + "name": "downloads", + "type": "java.lang.Integer", + "description": "Descargas realizadas", + "unit": "downloads" + }, + { + "name": "comments", + "type": "java.lang.Integer", + "description": "Número de comentarios", + "unit": "comments" + }, + { + "name": "uniqueVisitors", + "type": "java.lang.Double", + "description": "Número de visitantes únicos diarios al repositorio", + "unit": "visitors/day" + }, + { + "name": "uniqueClones", + "type": "java.lang.Double", + "description": "Número de clonaciones únicas diarias del repositorio", + "unit": "clones/day" + } + ], + "indicators": [ + { + "name": "pullReqGlory", + "type": "java.lang.Double", + "description": "Ratio de pull request aceptados frente a solicitados", + "unit": "ratio", + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } + }, + { + "name": "commentsInterest", + "type": "java.lang.Double", + "description": "Ratio de comentarios con más de 1 respuesta frente al número total de comentarios", + "unit": "ratio", + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } + }, + { + "name": "GithubTraffic", + "type": "java.lang.Double", + "description": "Tasa de conversión de visitantes únicos a clonaciones únicas en un repositorio GitHub", + "unit": "conversion rate", + "limits": { + "ok": 50, + "warning": 10, + "critical": 0 + } + } + ] +}