Skip to content

Commit d7c47e6

Browse files
authored
Add usage metrics for central release 2025.3 (#1662)
* Added num_entity_bulk_deletes usage metric * Counting max geo data in cache in a single form: max_geo_per_form * Updating form version * Add num_datasets_with_geometry to count system-wide * Count enitites with geometry per dataset: num_entities_with_geometry * Queries to count entity forms, specified by action and repeat
1 parent 5e2832b commit d7c47e6

File tree

4 files changed

+453
-42
lines changed

4 files changed

+453
-42
lines changed

config/default.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"analytics": {
3131
"url": "https://data.getodk.cloud/v1/key/eOZ7S4bzyUW!g1PF6dIXsnSqktRuewzLTpmc6ipBtRq$LDfIMTUKswCexvE0UwJ9/projects/1/forms/odk-analytics/submissions",
3232
"formId": "odk-analytics",
33-
"version": "v2025.2.0_1"
33+
"version": "v2025.3.0_1"
3434
},
3535
"s3blobStore": {}
3636
},

lib/data/analytics.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const metricsTemplate = {
2121
"total": 0
2222
},
2323
"num_questions_biggest_form": {},
24+
"max_geo_per_form": 0,
2425
"num_audit_log_entries": {
2526
"recent": 0,
2627
"total": 0
@@ -50,7 +51,12 @@ const metricsTemplate = {
5051
"num_xml_only_form_defs": 0,
5152
"num_blob_files": 0,
5253
"num_blob_files_on_s3": 0,
53-
"num_reset_failed_to_pending_count": 0
54+
"num_reset_failed_to_pending_count": 0,
55+
"num_entity_bulk_deletes": {
56+
"recent": 0,
57+
"total": 0
58+
},
59+
"num_datasets_with_geometry": 0
5460
},
5561
"projects": [
5662
{
@@ -115,7 +121,12 @@ const metricsTemplate = {
115121
"num_closed_forms": {
116122
"recent": 0,
117123
"total": 0
118-
}
124+
},
125+
"num_entity_create_forms": 0,
126+
"num_repeat_entity_create_forms": 0,
127+
"num_entity_update_forms": 0,
128+
"num_repeat_entity_update_forms": 0,
129+
"num_entity_create_update_forms": 0,
119130
},
120131
"submissions": {
121132
"num_submissions_received": {
@@ -197,7 +208,11 @@ const metricsTemplate = {
197208
total: 0,
198209
recent: 0
199210
},
200-
"biggest_bulk_upload": 0
211+
"biggest_bulk_upload": 0,
212+
"num_entities_with_geometry": {
213+
total: 0,
214+
recent: 0
215+
}
201216
}]
202217
}
203218
]

lib/model/query/analytics.js

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
const config = require('config');
1111
const { sql } = require('slonik');
12-
const { clone } = require('ramda');
12+
const { clone, omit } = require('ramda');
1313
const { runSequentially } = require('../../util/promise');
1414
const { metricsTemplate } = require('../../data/analytics');
1515
const oidc = require('../../util/oidc');
@@ -344,6 +344,33 @@ on forms."xmlFormId" = deleted_form_ids."xml_form_id" and
344344
where forms."deletedAt" is null
345345
group by forms."projectId"`);
346346

347+
const maxGeoPerForm = () => ({ oneFirst }) => oneFirst(sql`
348+
SELECT MAX(geo_count)
349+
FROM (
350+
SELECT COUNT(*) as geo_count, fd."formId"
351+
FROM submission_field_extract_geo_cache AS sc
352+
JOIN submission_defs AS sd
353+
ON sd.id = sc."submission_def_id"
354+
JOIN form_defs AS fd
355+
ON sd."formDefId" = fd.id
356+
GROUP BY fd."formId", sc.fieldhash
357+
) as geo_counts`);
358+
359+
const countFormsCreateUpdateEntitiesFromRepeats = () => ({ all }) => all(sql`
360+
SELECT
361+
COUNT(DISTINCT CASE WHEN dfd.actions ? 'create' THEN f.id END) AS num_entity_create_forms,
362+
COUNT(DISTINCT CASE WHEN dfd.actions ? 'create' AND dfd."isRepeat" THEN f.id END) AS num_repeat_entity_create_forms,
363+
COUNT(DISTINCT CASE WHEN dfd.actions ? 'update' THEN f.id END) AS num_entity_update_forms,
364+
COUNT(DISTINCT CASE WHEN dfd.actions ? 'update' AND dfd."isRepeat" THEN f.id END) AS num_repeat_entity_update_forms,
365+
COUNT(DISTINCT CASE WHEN dfd.actions ? 'create' AND dfd.actions ? 'update' THEN f.id END) AS num_entity_create_update_forms,
366+
f."projectId"
367+
FROM forms AS f
368+
JOIN form_defs AS fd ON f."currentDefId" = fd."id"
369+
JOIN dataset_form_defs AS dfd ON dfd."formDefId" = fd.id
370+
WHERE f."deletedAt" IS NULL
371+
GROUP BY f."projectId"
372+
ORDER BY f."projectId"`);
373+
347374
// Submissions
348375
const countSubmissions = () => ({ all }) => all(sql`
349376
select f."projectId", count(s.id) as total,
@@ -540,7 +567,7 @@ const getDatasets = () => ({ all }) => all(sql`
540567

541568
const getDatasetEvents = () => ({ all }) => all(sql`
542569
SELECT
543-
ds.id, ds."projectId",
570+
ds.id "datasetId", ds."projectId",
544571
COUNT (*) num_bulk_create_events_total,
545572
SUM (CASE WHEN audits."loggedAt" >= ${_cutoffDate} THEN 1 ELSE 0 END) num_bulk_create_events_recent,
546573
MAX (CAST(sources."details"->'count' AS integer)) AS biggest_bulk_upload
@@ -553,7 +580,7 @@ GROUP BY ds.id, ds."projectId"
553580

554581
const getDatasetProperties = () => ({ all }) => all(sql`
555582
SELECT
556-
ds.id, ds."projectId", COUNT(DISTINCT p.id) num_properties
583+
ds.id "datasetId", ds."projectId", COUNT(DISTINCT p.id) num_properties
557584
FROM datasets ds
558585
LEFT JOIN ds_properties p ON p."datasetId" = ds.id AND p."publishedAt" IS NOT NULL
559586
WHERE ds."publishedAt" IS NOT NULL
@@ -661,6 +688,36 @@ const countOwnerOnlyDatasets = () => ({ oneFirst }) => oneFirst(sql`
661688
SELECT COUNT(1) FROM datasets where "ownerOnly" = true;
662689
`);
663690

691+
const countDatasetsWithGeometry = () => ({ oneFirst }) => oneFirst(sql`
692+
SELECT COUNT(DISTINCT "datasetId") FROM ds_properties WHERE name = 'geometry';
693+
`);
694+
695+
const countEntitiesWithGeometry = () => ({ all }) => all(sql`
696+
SELECT
697+
COUNT(*) AS total,
698+
COUNT(CASE WHEN e."createdAt" >= ${_cutoffDate}
699+
THEN 1
700+
ELSE null
701+
END) AS recent,
702+
ds."projectId",
703+
ds.id as "datasetId"
704+
FROM entities AS e
705+
JOIN entity_defs AS ed ON ed."entityId" = e.id AND ed.current = true
706+
JOIN datasets AS ds ON ds.id = e."datasetId"
707+
WHERE ed.data ? 'geometry'
708+
AND e."deletedAt" IS NULL
709+
AND ds."publishedAt" IS NOT NULL
710+
GROUP BY ds.id, ds."projectId"`);
711+
712+
const countEntityBulkDeletes = () => ({ one }) => one(sql`
713+
SELECT
714+
count(*) AS total,
715+
count(CASE WHEN "loggedAt" >= ${_cutoffDate}
716+
THEN 1
717+
ELSE null
718+
END) AS recent
719+
FROM audits WHERE action = 'entity.bulk.delete'`);
720+
664721
// Other
665722
const getProjectsWithDescriptions = () => ({ all }) => all(sql`
666723
select id as "projectId", length(trim(description)) as description_length from projects where coalesce(trim(description),'')!=''`);
@@ -676,6 +733,7 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
676733
Analytics.countFormsInStates,
677734
Analytics.countFormsWebformsEnabled,
678735
Analytics.countReusedFormIds,
736+
Analytics.countFormsCreateUpdateEntitiesFromRepeats,
679737
Analytics.countSubmissions,
680738
Analytics.countSubmissionReviewStates,
681739
Analytics.countSubmissionsEdited,
@@ -684,11 +742,12 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
684742
Analytics.getProjectsWithDescriptions,
685743
Analytics.getDatasets,
686744
Analytics.getDatasetEvents,
687-
Analytics.getDatasetProperties
745+
Analytics.getDatasetProperties,
746+
Analytics.countEntitiesWithGeometry,
688747
]).then(([ userRoles, appUsers, deviceIds, pubLinks,
689-
forms, formGeoRepeats, formsEncrypt, formStates, webforms, reusedIds,
748+
forms, formGeoRepeats, formsEncrypt, formStates, webforms, reusedIds, entityForms,
690749
subs, subStates, subEdited, subComments, subUsers,
691-
projWithDesc, datasets, datasetEvents, datasetProperties ]) => {
750+
projWithDesc, datasets, datasetEvents, datasetProperties, entitiesWithGeometry ]) => {
692751
const projects = {};
693752

694753
// users
@@ -762,6 +821,11 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
762821
project.forms.num_reused_form_ids = row.total;
763822
}
764823

824+
for (const row of entityForms) {
825+
const project = _getProject(projects, row.projectId);
826+
Object.assign(project.forms, omit(['projectId'], row));
827+
}
828+
765829
// submissions
766830
for (const row of subs) {
767831
const project = _getProject(projects, row.projectId);
@@ -798,21 +862,28 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
798862
project.submissions.num_submissions_from_web_users = { total: row.web_user_total, recent: row.web_user_recent };
799863
}
800864

865+
866+
// Helper function
867+
const findDataset = (metric, dataset, defaultValue) =>
868+
metric.find(d => (d.datasetId === dataset.id)) || defaultValue;
869+
801870
// datasets
802871
for (const row of datasets) {
803872
const project = _getProject(projects, row.projectId);
804873

805874
// Additional dataset metrics are returned in a separate query. Look up the correct dataset/project row.
806-
const eventsRow = datasetEvents.find(d => (d.projectId === row.projectId && d.id === row.id)) ||
807-
{ num_bulk_create_events_total: 0, num_bulk_create_events_recent: 0, biggest_bulk_upload: 0 };
875+
const eventsRow = findDataset(datasetEvents, row, { num_bulk_create_events_total: 0, num_bulk_create_events_recent: 0, biggest_bulk_upload: 0 });
808876

809877
// Properties row
810-
const propertiesRow = datasetProperties.find(d => (d.projectId === row.projectId && d.id === row.id)) ||
811-
{ num_properties: 0 };
878+
const propertiesRow = findDataset(datasetProperties, row, { num_properties: 0 });
879+
880+
// Entities with geometry
881+
const entitiesWithGeometryRow = findDataset(entitiesWithGeometry, row, { total: 0, recent: 0 });
812882

813883
project.datasets.push({
814884
id: row.id,
815885
num_properties: propertiesRow.num_properties,
886+
num_entities_with_geometry: { total: entitiesWithGeometryRow.total, recent: entitiesWithGeometryRow.recent },
816887
num_creation_forms: row.num_creation_forms,
817888
num_followup_forms: row.num_followup_forms,
818889
num_entities: { total: row.num_entities_total, recent: row.num_entities_recent },
@@ -837,6 +908,7 @@ const projectMetrics = () => (({ Analytics }) => runSequentially([
837908
});
838909
}
839910

911+
840912
// other
841913
for (const row of projWithDesc) {
842914
const project = _getProject(projects, row.projectId);
@@ -852,6 +924,7 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
852924
Analytics.databaseSize,
853925
Analytics.encryptedProjects,
854926
Analytics.biggestForm,
927+
Analytics.maxGeoPerForm,
855928
Analytics.countAdmins,
856929
Analytics.auditLogs,
857930
Analytics.archivedProjects,
@@ -870,12 +943,16 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
870943
Analytics.measureEntityProcessingTime,
871944
Analytics.measureMaxEntityBranchTime,
872945
Analytics.countOwnerOnlyDatasets,
946+
Analytics.countDatasetsWithGeometry,
947+
Analytics.countEntityBulkDeletes,
873948
Analytics.projectMetrics
874-
]).then(([db, encrypt, bigForm, admins, audits,
949+
]).then(([db, encrypt, bigForm, maxGeo, admins, audits,
875950
archived, managers, viewers, collectors,
876951
caAttachments, caFailures, caRows, xmlDefs, blobFiles, resetFailedToPending,
877952
oeBranches, oeInterruptedBranches, oeBacklogEvents, oeProcessingTime, oeBranchTime,
878953
ownerOnlyDatasets,
954+
datasetsWithGeometry,
955+
entityBulkDeletes,
879956
projMetrics]) => {
880957
const metrics = clone(metricsTemplate);
881958
// system
@@ -891,6 +968,7 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
891968
for (const [key, value] of Object.entries(encrypt))
892969
metrics.system.num_projects_encryption[key] = value;
893970
metrics.system.num_questions_biggest_form = bigForm;
971+
metrics.system.max_geo_per_form = maxGeo || 0;
894972
for (const [key, value] of Object.entries(admins))
895973
metrics.system.num_admins[key] = value;
896974

@@ -930,6 +1008,12 @@ const previewMetrics = () => (({ Analytics }) => runSequentially([
9301008
// 2025.2.0 owner only entity lists/datasets
9311009
metrics.system.num_owner_only_datasets = ownerOnlyDatasets;
9321010

1011+
// 2025.3.0 entity bulk delete audit logs
1012+
metrics.system.num_entity_bulk_deletes = entityBulkDeletes;
1013+
1014+
// 2025.3.0 datasets with geometry property
1015+
metrics.system.num_datasets_with_geometry = datasetsWithGeometry || 0;
1016+
9331017
return metrics;
9341018
}));
9351019

@@ -968,6 +1052,8 @@ module.exports = {
9681052
countFormFieldTypes,
9691053
countFormsInStates,
9701054
countFormsWebformsEnabled,
1055+
maxGeoPerForm,
1056+
countFormsCreateUpdateEntitiesFromRepeats,
9711057
countPublicLinks,
9721058
countReusedFormIds,
9731059
countSubmissions,
@@ -993,5 +1079,8 @@ module.exports = {
9931079
measureEntityProcessingTime,
9941080
measureElapsedEntityTime,
9951081
measureMaxEntityBranchTime,
996-
countOwnerOnlyDatasets
1082+
countOwnerOnlyDatasets,
1083+
countDatasetsWithGeometry,
1084+
countEntitiesWithGeometry,
1085+
countEntityBulkDeletes
9971086
};

0 commit comments

Comments
 (0)