From e376193bc559650fc7065e2e57c13bda2316752f Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 12 Sep 2025 11:20:03 -0500 Subject: [PATCH 1/4] stats_counts context api --- specifyweb/backend/context/urls.py | 2 +- specifyweb/backend/context/views.py | 16 +++++- .../components/InitialContext/systemInfo.ts | 52 +++++++++++++------ 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/specifyweb/backend/context/urls.py b/specifyweb/backend/context/urls.py index 59ee69b16c1..d94248603b7 100644 --- a/specifyweb/backend/context/urls.py +++ b/specifyweb/backend/context/urls.py @@ -19,6 +19,7 @@ re_path(r'^api_endpoints.json$', views.api_endpoints), re_path(r'^api_endpoints_all.json$', views.api_endpoints_all), re_path(r'^user.json$', views.user), + re_path(r'^stats_counts.json$', views.stats_counts), re_path(r'^system_info.json$', views.system_info), re_path(r'^server_time.json$', views.get_server_time), re_path(r'^domain.json$', views.domain), @@ -40,5 +41,4 @@ path('collection_resource/', collection_resources.collection_resources), path('collection_resource//', collection_resources.collection_resource), - ] diff --git a/specifyweb/backend/context/views.py b/specifyweb/backend/context/views.py index bed109ce506..a10cba9dc42 100644 --- a/specifyweb/backend/context/views.py +++ b/specifyweb/backend/context/views.py @@ -26,7 +26,7 @@ PermissionTargetAction, \ check_permission_targets, skip_collection_access_check, query_pt, \ CollectionAccessPT -from specifyweb.specify.models import Collection, Institution, \ +from specifyweb.specify.models import Collection, Collectionobject, Institution, \ Specifyuser, Spprincipal, Spversion, Collectionobjecttype from specifyweb.specify.models_utils.schema import base_schema from specifyweb.specify.models_utils.serialize_datamodel import datamodel_to_json @@ -1018,3 +1018,17 @@ def schema_language(request): dict(zip(('language', 'country', 'variant'), row)) for row in schema_languages ], safe=False) + +@require_http_methods(['GET', 'HEAD']) +@cache_control(max_age=86400, public=True) +@login_maybe_required +def stats_counts(request): + """Get the count of collection objects, collections, and users.""" + co_count = Collectionobject.objects.count() + collection_count = Collection.objects.count() + user_count = Specifyuser.objects.count() + return JsonResponse({ + 'Collectionobject': co_count, + 'Collection': collection_count, + 'Specifyuser': user_count, + }) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index 8ff7a789077..32e3d4341f0 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -25,28 +25,48 @@ type SystemInfo = { readonly discipline_type: string; }; +type StatsCounts = { + readonly Collectionobject: number; + readonly Collection: number; + readonly Specifyuser: number; +}; + let systemInfo: SystemInfo; export const fetchContext = load( '/context/system_info.json', 'application/json' -).then((data) => { +).then(async (data) => { systemInfo = data; - if (systemInfo.stats_url !== null) - ping( + + if (systemInfo.stats_url !== null) { + let counts: StatsCounts | null = null; + try { + counts = await load('/context/stats_counts.json', 'application/json'); + } catch { + // If counts fetch fails, proceed without them. + counts = null; + } + + const params = { + version: systemInfo.version, + dbVersion: systemInfo.database_version, + institution: systemInfo.institution, + institutionGUID: systemInfo.institution_guid, + discipline: systemInfo.discipline, + collection: systemInfo.collection, + collectionGUID: systemInfo.collection_guid, + isaNumber: systemInfo.isa_number, + disciplineType: systemInfo.discipline_type, + collectionObjectCount: counts?.Collectionobject ?? 0, + collectionCount: counts?.Collection ?? 0, + userCount: counts?.Specifyuser ?? 0, + }; + + await ping( formatUrl( systemInfo.stats_url, - { - version: systemInfo.version, - dbVersion: systemInfo.database_version, - institution: systemInfo.institution, - institutionGUID: systemInfo.institution_guid, - discipline: systemInfo.discipline, - collection: systemInfo.collection, - collectionGUID: systemInfo.collection_guid, - isaNumber: systemInfo.isa_number, - disciplineType: systemInfo.discipline_type, - }, + params, /* * I don't know if the receiving server handles GET parameters in a * case-sensitive way. Thus, don't convert keys to lower case, but leave @@ -56,7 +76,9 @@ export const fetchContext = load( ), { errorMode: 'silent' } ).catch(softFail); + } + return systemInfo; }); -export const getSystemInfo = (): SystemInfo => systemInfo; +export const getSystemInfo = (): SystemInfo => systemInfo; \ No newline at end of file From 6f985d36e21c450658edb0eae5ef4b57c2353bde Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 12 Sep 2025 18:00:43 +0000 Subject: [PATCH 2/4] Lint code with ESLint and Prettier Triggered by e376193bc559650fc7065e2e57c13bda2316752f on branch refs/heads/issue-7414 --- .../js_src/lib/components/InitialContext/systemInfo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index 32e3d4341f0..8a2bb8e7408 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -48,7 +48,7 @@ export const fetchContext = load( counts = null; } - const params = { + const parameters = { version: systemInfo.version, dbVersion: systemInfo.database_version, institution: systemInfo.institution, @@ -66,7 +66,7 @@ export const fetchContext = load( await ping( formatUrl( systemInfo.stats_url, - params, + parameters, /* * I don't know if the receiving server handles GET parameters in a * case-sensitive way. Thus, don't convert keys to lower case, but leave From 0fb21b593e933439b92d8394623e76a688b135d7 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 14 Oct 2025 15:28:01 -0500 Subject: [PATCH 3/4] configure second stats request to new lambda function endpoint --- specifyweb/backend/context/views.py | 1 + .../components/InitialContext/systemInfo.ts | 27 +++++++++++++++++++ specifyweb/settings/specify_settings.py | 2 ++ 3 files changed, 30 insertions(+) diff --git a/specifyweb/backend/context/views.py b/specifyweb/backend/context/views.py index a10cba9dc42..a94f29eacfe 100644 --- a/specifyweb/backend/context/views.py +++ b/specifyweb/backend/context/views.py @@ -655,6 +655,7 @@ def system_info(request): database_version=spversion.appversion, schema_version=spversion.schemaversion, stats_url=settings.STATS_URL, + stats_2_url=settings.STATS_2_URL, database=settings.DATABASE_NAME, institution=institution.name, institution_guid=institution.guid, diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index 8a2bb8e7408..f25073fbc04 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -22,6 +22,7 @@ type SystemInfo = { readonly institution_guid: LocalizedString; readonly isa_number: LocalizedString; readonly stats_url: string | null; + readonly stats_2_url: string | null; readonly discipline_type: string; }; @@ -33,6 +34,21 @@ type StatsCounts = { let systemInfo: SystemInfo; +function buildStatsLambdaUrl(base: string | null | undefined): string | null { + if (!base) return null; + let u = base.trim(); + + if (!/^https?:\/\//i.test(u)) u = `https://${u}`; + + const hasRoute = /\/(prod|default)\/[^/\s]+/.test(u); + if (!hasRoute) { + const stage = 'prod'; + const route = 'AggrgatedSp7Stats'; + u = u.replace(/\/$/, '') + `/${stage}/${route}`; + } + return u; +} + export const fetchContext = load( '/context/system_info.json', 'application/json' @@ -76,6 +92,17 @@ export const fetchContext = load( ), { errorMode: 'silent' } ).catch(softFail); + + // await ping( + // formatUrl(systemInfo.stats_2_url, parameters, false), + // { errorMode: 'silent' } + // ).catch(softFail); + + const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); + if (lambdaUrl) { + await ping(formatUrl(lambdaUrl, parameters, false), { errorMode: 'silent' }) + .catch(softFail); + } } return systemInfo; diff --git a/specifyweb/settings/specify_settings.py b/specifyweb/settings/specify_settings.py index b83bdda9007..81e28d96215 100644 --- a/specifyweb/settings/specify_settings.py +++ b/specifyweb/settings/specify_settings.py @@ -102,6 +102,8 @@ # Usage stats are transmitted to the following address. # Set to None to disable. STATS_URL = "https://stats.specifycloud.org/capture" +# STATS_2_URL = "https://stats-2.specifycloud.org/prod/AggrgatedSp7Stats" +STATS_2_URL = "pj9lpoo1pc.execute-api.us-east-1.amazonaws.com" # Workbench uploader log directory. # Must exist and be writeable by the web server process. From 75751f8a83d05ad425a58a42e38b0e42dd266728 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 14 Oct 2025 20:32:33 +0000 Subject: [PATCH 4/4] Lint code with ESLint and Prettier Triggered by 0fb21b593e933439b92d8394623e76a688b135d7 on branch refs/heads/issue-7414 --- .../lib/components/InitialContext/systemInfo.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index f25073fbc04..da2414c3854 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -40,11 +40,11 @@ function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!/^https?:\/\//i.test(u)) u = `https://${u}`; - const hasRoute = /\/(prod|default)\/[^/\s]+/.test(u); + const hasRoute = /\/(prod|default)\/[^\s/]+/.test(u); if (!hasRoute) { const stage = 'prod'; const route = 'AggrgatedSp7Stats'; - u = u.replace(/\/$/, '') + `/${stage}/${route}`; + u = `${u.replace(/\/$/, '') }/${stage}/${route}`; } return u; } @@ -93,10 +93,12 @@ export const fetchContext = load( { errorMode: 'silent' } ).catch(softFail); - // await ping( - // formatUrl(systemInfo.stats_2_url, parameters, false), - // { errorMode: 'silent' } - // ).catch(softFail); + /* + * Await ping( + * formatUrl(systemInfo.stats_2_url, parameters, false), + * { errorMode: 'silent' } + * ).catch(softFail); + */ const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); if (lambdaUrl) {