From 9f2e3b1874821029ae08836befde856b56dd3e43 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 23 Oct 2026 15:44:35 -0700 Subject: [PATCH 1/3] Describe Flow and Protein usage metrics --- flow/src/org/labkey/flow/FlowMetricArea.java | 36 +++++++++++++++++++ flow/src/org/labkey/flow/FlowModule.java | 4 ++- .../AnalysisScriptController.java | 4 +-- .../org/labkey/flow/persist/FlowManager.java | 28 +++++++++++++++ .../src/org/labkey/protein/ProteinModule.java | 3 +- 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 flow/src/org/labkey/flow/FlowMetricArea.java diff --git a/flow/src/org/labkey/flow/FlowMetricArea.java b/flow/src/org/labkey/flow/FlowMetricArea.java new file mode 100644 index 000000000..9fbe1bf2c --- /dev/null +++ b/flow/src/org/labkey/flow/FlowMetricArea.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.flow; + +import org.labkey.api.usageMetrics.SimpleMetricArea; + +public enum FlowMetricArea implements SimpleMetricArea +{ + SAMPLE_IMPORT("SampleImport", "Analysis import wizard submissions after the user changed the target study (StudyLinkChanged)"); + + private final Definition _definition; + + FlowMetricArea(String areaName, String description) + { + _definition = new Definition(FlowModule.NAME, areaName, description); + } + + @Override + public Definition getDefinition() + { + return _definition; + } +} diff --git a/flow/src/org/labkey/flow/FlowModule.java b/flow/src/org/labkey/flow/FlowModule.java index ec5c7bed5..bdcd14d87 100644 --- a/flow/src/org/labkey/flow/FlowModule.java +++ b/flow/src/org/labkey/flow/FlowModule.java @@ -37,6 +37,7 @@ import org.labkey.api.query.QuerySchema; import org.labkey.api.reports.ReportService; import org.labkey.api.search.SearchService; +import org.labkey.api.usageMetrics.SimpleMetricsService; import org.labkey.api.usageMetrics.UsageMetricsService; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.DefaultWebPartFactory; @@ -297,8 +298,9 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext) if (null != svc) { FlowManager mgr = FlowManager.get(); - svc.registerUsageMetrics(NAME, mgr::getUsageMetrics); + svc.registerUsageMetrics(NAME, mgr::addUsageMetrics); } + SimpleMetricsService.get().describeFeatureAreas(FlowMetricArea.values()); FlowSchema.registerContainerListener(); } diff --git a/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java b/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java index e8abee539..99b3248bd 100644 --- a/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java +++ b/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java @@ -65,7 +65,7 @@ import org.labkey.api.view.RedirectException; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.api.view.template.PageConfig; -import org.labkey.flow.FlowModule; +import org.labkey.flow.FlowMetricArea; import org.labkey.flow.FlowPreference; import org.labkey.flow.analysis.model.ExternalAnalysis; import org.labkey.flow.analysis.model.FCS; @@ -1399,7 +1399,7 @@ private void stepChooseAnalysis(ImportAnalysisForm form, BindException errors) if (form.getStudyChanged()) { // Estabilish & increment a metric to measure usage of the Study Linkage feature of the Flow data import process - SimpleMetricsService.get().increment(FlowModule.NAME, "SampleImport", "StudyLinkChanged"); + SimpleMetricsService.get().increment(FlowMetricArea.SAMPLE_IMPORT, "StudyLinkChanged"); } if (errors.hasErrors()) diff --git a/flow/src/org/labkey/flow/persist/FlowManager.java b/flow/src/org/labkey/flow/persist/FlowManager.java index 4650d5c50..a9cea4b26 100644 --- a/flow/src/org/labkey/flow/persist/FlowManager.java +++ b/flow/src/org/labkey/flow/persist/FlowManager.java @@ -63,6 +63,7 @@ import org.labkey.api.security.permissions.AbstractContainerScopingTest; import org.labkey.api.security.roles.EditorRole; import org.labkey.api.security.roles.ReaderRole; +import org.labkey.api.usageMetrics.UsageMetrics; import org.labkey.api.util.DateUtil; import org.labkey.api.util.FileUtil; import org.labkey.api.util.IntegerUtils; @@ -1264,6 +1265,33 @@ public Map getUsageMetrics() }); } + private static final Map USAGE_METRIC_DESCRIPTIONS = Map.ofEntries( + Map.entry("flowTempTableCount", "Flow temp tables (ffo* and fbg*) currently in the database's temp schema"), + Map.entry("keyword", "Keyword attributes across the server: total, aliases, distinct names, and distinct names ignoring case; aliascount is null when the table is empty"), + Map.entry("statistic", "Statistic attributes across the server: total, aliases, distinct names, and distinct names ignoring case; aliascount is null when the table is empty"), + Map.entry("graph", "Graph attributes across the server: total, aliases, distinct names, and distinct names ignoring case; aliascount is null when the table is empty"), + Map.entry("scriptRuns", "Runs created by an analysis script, summed across Flow-enabled folders: run count, latest created date, and FCS analysis, compensation control, and FCS file counts"), + Map.entry("workspaceRuns", "Runs imported from a FlowJo workspace, summed across Flow-enabled folders: run count, latest created date, and FCS analysis, compensation control, and FCS file counts"), + Map.entry("externalRuns", "Runs imported from an external analysis archive, summed across Flow-enabled folders: run count, latest created date, and FCS analysis, compensation control, and FCS file counts"), + Map.entry("fcsFileRuns", "Keywords (FCS file import) runs, summed across Flow-enabled folders: run count, latest created date, and FCS analysis, compensation control, and FCS file counts"), + Map.entry("analysisFolderCount", "Flow analysis folders, summed across Flow-enabled folders"), + Map.entry("analysisScriptCount", "Flow analysis scripts, summed across Flow-enabled folders"), + Map.entry("protocol", "Flow-enabled folders whose protocol has each setting: ICS metadata, complete study metadata, complete background, sample join fields, case-sensitive keywords, case-sensitive stats and graphs"), + Map.entry("sampleCount", "Samples in flow protocol sample types, summed across Flow-enabled folders; absent when no protocol has a sample type") + ); + + public UsageMetrics addUsageMetrics(UsageMetrics metrics) + { + // Describe every key, including per-folder ones that are absent when no folder has Flow enabled + USAGE_METRIC_DESCRIPTIONS.forEach(metrics::describe); + getUsageMetrics().forEach((key, value) -> { + String description = USAGE_METRIC_DESCRIPTIONS.get(key); + assert description != null : "No usage metric description for Flow key " + key; + metrics.put(key, value, description); + }); + return metrics; + } + /** * merge the values from map into the target map. * - numbers are summed diff --git a/protein/src/org/labkey/protein/ProteinModule.java b/protein/src/org/labkey/protein/ProteinModule.java index 74d39960f..d405d4826 100644 --- a/protein/src/org/labkey/protein/ProteinModule.java +++ b/protein/src/org/labkey/protein/ProteinModule.java @@ -54,7 +54,6 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; public class ProteinModule extends DefaultModule @@ -122,7 +121,7 @@ public void doStartup(ModuleContext moduleContext) ContainerManager.addContainerListener(new ProteinContainerListener()); PipelineService service = PipelineService.get(); service.registerPipelineProvider(new ProteinAnnotationPipelineProvider(this)); - UsageMetricsService.get().registerUsageMetrics(getName(), () -> Map.of("hasGeneOntologyData", new TableSelector(ProteinSchema.getTableInfoGoTerm()).exists())); + UsageMetricsService.get().registerUsageMetrics(getName(), metrics -> metrics.put("hasGeneOntologyData", new TableSelector(ProteinSchema.getTableInfoGoTerm()).exists(), "Whether Gene Ontology terms have been loaded (server-wide)")); ProteinController.registerAdminConsoleLinks(); FileContentService fcs = FileContentService.get(); From 8c2421d75c3a5daed8b1cda12f38cfdcc77c86f4 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 23 Oct 2026 18:22:25 -0700 Subject: [PATCH 2/3] Claude review --- flow/src/org/labkey/flow/FlowMetricArea.java | 2 +- flow/src/org/labkey/flow/persist/FlowManager.java | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/flow/src/org/labkey/flow/FlowMetricArea.java b/flow/src/org/labkey/flow/FlowMetricArea.java index 9fbe1bf2c..d22903384 100644 --- a/flow/src/org/labkey/flow/FlowMetricArea.java +++ b/flow/src/org/labkey/flow/FlowMetricArea.java @@ -19,7 +19,7 @@ public enum FlowMetricArea implements SimpleMetricArea { - SAMPLE_IMPORT("SampleImport", "Analysis import wizard submissions after the user changed the target study (StudyLinkChanged)"); + SAMPLE_IMPORT("SampleImport", "Analysis import wizard events, by metric name: StudyLinkChanged (submitted after the user changed the target study)"); private final Definition _definition; diff --git a/flow/src/org/labkey/flow/persist/FlowManager.java b/flow/src/org/labkey/flow/persist/FlowManager.java index a9cea4b26..0c1e33c5e 100644 --- a/flow/src/org/labkey/flow/persist/FlowManager.java +++ b/flow/src/org/labkey/flow/persist/FlowManager.java @@ -1282,13 +1282,10 @@ public Map getUsageMetrics() public UsageMetrics addUsageMetrics(UsageMetrics metrics) { - // Describe every key, including per-folder ones that are absent when no folder has Flow enabled - USAGE_METRIC_DESCRIPTIONS.forEach(metrics::describe); - getUsageMetrics().forEach((key, value) -> { - String description = USAGE_METRIC_DESCRIPTIONS.get(key); - assert description != null : "No usage metric description for Flow key " + key; - metrics.put(key, value, description); - }); + // Per-folder keys are absent when no folder has Flow enabled + Map values = getUsageMetrics(); + USAGE_METRIC_DESCRIPTIONS.forEach((key, description) -> + metrics.reportedIf(values.containsKey(key)).put(key, values.get(key), description)); return metrics; } From e1a201384f2a4f3623d66608c31d29e08c249c48 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Mon, 26 Oct 2026 18:49:28 -0700 Subject: [PATCH 3/3] Skip usage metric queries when only collecting descriptions --- .../controllers/executescript/AnalysisScriptController.java | 2 +- protein/src/org/labkey/protein/ProteinModule.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java b/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java index 92524ff05..6585b9102 100644 --- a/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java +++ b/flow/src/org/labkey/flow/controllers/executescript/AnalysisScriptController.java @@ -1458,7 +1458,7 @@ private void stepChooseAnalysis(ImportAnalysisForm form, BindException errors) Container targetStudy = getTargetStudy(form.getTargetStudy(), errors); if (form.getStudyChanged()) { - // Estabilish & increment a metric to measure usage of the Study Linkage feature of the Flow data import process + // Establish & increment a metric to measure usage of the Study Linkage feature of the Flow data import process SimpleMetricsService.get().increment(FlowMetricArea.SAMPLE_IMPORT, "StudyLinkChanged"); } diff --git a/protein/src/org/labkey/protein/ProteinModule.java b/protein/src/org/labkey/protein/ProteinModule.java index d405d4826..26b4dfca4 100644 --- a/protein/src/org/labkey/protein/ProteinModule.java +++ b/protein/src/org/labkey/protein/ProteinModule.java @@ -121,7 +121,7 @@ public void doStartup(ModuleContext moduleContext) ContainerManager.addContainerListener(new ProteinContainerListener()); PipelineService service = PipelineService.get(); service.registerPipelineProvider(new ProteinAnnotationPipelineProvider(this)); - UsageMetricsService.get().registerUsageMetrics(getName(), metrics -> metrics.put("hasGeneOntologyData", new TableSelector(ProteinSchema.getTableInfoGoTerm()).exists(), "Whether Gene Ontology terms have been loaded (server-wide)")); + UsageMetricsService.get().registerUsageMetrics(getName(), metrics -> metrics.put("hasGeneOntologyData", () -> new TableSelector(ProteinSchema.getTableInfoGoTerm()).exists(), "Whether Gene Ontology terms have been loaded (server-wide)")); ProteinController.registerAdminConsoleLinks(); FileContentService fcs = FileContentService.get();