From 76c66602e8dc75e49993b94e853400325032afff Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 12 Feb 2026 13:16:45 -0600 Subject: [PATCH 01/11] safe implementation of stats request --- .../lib/components/InitialContext/stats.ts | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index 4bf4d164617..60be2c71ce8 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -10,6 +10,10 @@ type StatsCounts = { readonly Specifyuser: number; }; +const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; +const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; +const stats2RequestTimeoutMs = 5_000; + function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; let u = base.trim(); @@ -25,6 +29,53 @@ function buildStatsLambdaUrl(base: string | null | undefined): string | null { return u; } +function buildStats2RequestKey(lambdaUrl: string, collectionGuid: string): string { + return `${stats2RequestKeyPrefix}:${collectionGuid}:${lambdaUrl}`; +} + +function shouldSendStats2Request(storageKey: string, now = Date.now()): boolean { + if (globalThis.localStorage === undefined) return true; + + try { + const previousRequestAt = globalThis.localStorage.getItem(storageKey); + if (previousRequestAt === null) return true; + const parsed = Number(previousRequestAt); + if (!Number.isFinite(parsed)) return true; + if (parsed > now) return true; + return now - parsed >= stats2RequestIntervalMs; + } catch { + return true; + } +} + +function recordStats2Request(storageKey: string, now = Date.now()): void { + if (globalThis.localStorage === undefined) return; + + try { + globalThis.localStorage.setItem(storageKey, `${now}`); + } catch {} +} + +function pingInBackground(url: string): void { + const controller = + typeof globalThis.AbortController === 'function' + ? new globalThis.AbortController() + : undefined; + const timeoutId = + controller === undefined + ? undefined + : globalThis.setTimeout(() => controller.abort(), stats2RequestTimeoutMs); + + void ping(url, { + errorMode: 'silent', + ...(controller === undefined ? {} : { signal: controller.signal }), + }) + .catch(softFail) + .finally(() => { + if (timeoutId !== undefined) globalThis.clearTimeout(timeoutId); + }); +} + export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { if (systemInfo === undefined) { return; @@ -74,8 +125,20 @@ export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); if (lambdaUrl) { - await ping(formatUrl(lambdaUrl, parameters, false), { - errorMode: 'silent', - }).catch(softFail); + const storageKey = buildStats2RequestKey( + lambdaUrl, + `${systemInfo.collection_guid}` + ); + if (!shouldSendStats2Request(storageKey)) { + return; + } + recordStats2Request(storageKey); + pingInBackground(formatUrl(lambdaUrl, parameters, false)); } }); + +export const exportsForTests = { + buildStats2RequestKey, + shouldSendStats2Request, + recordStats2Request, +}; From d0aedc6c6d5a4046e47e383eb636a550284fbc7c Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 12 Feb 2026 19:20:48 +0000 Subject: [PATCH 02/11] Lint code with ESLint and Prettier Triggered by 76c66602e8dc75e49993b94e853400325032afff on branch refs/heads/issue-7693 --- .../frontend/js_src/lib/components/InitialContext/stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index 60be2c71ce8..ee09ee18b39 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -12,7 +12,7 @@ type StatsCounts = { const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; -const stats2RequestTimeoutMs = 5_000; +const stats2RequestTimeoutMs = 5000; function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; From 429bf37aa1fcc362c6ef6eb3fc659b1c3178493d Mon Sep 17 00:00:00 2001 From: alec_dev Date: Mon, 16 Feb 2026 15:30:01 -0600 Subject: [PATCH 03/11] add new stats lambda function url --- .../js_src/lib/components/InitialContext/stats.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index ee09ee18b39..084a00d5c16 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -13,19 +13,13 @@ type StatsCounts = { const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; const stats2RequestTimeoutMs = 5000; +const stats2LambdaFunctionUrl = 'https://hvf3gvyu6q3f3mkf6y5xlddstq0xmuel.lambda-url.us-east-1.on.aws/'; 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; } @@ -123,7 +117,7 @@ export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { { errorMode: 'silent' } ).catch(softFail); - const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); + const lambdaUrl = buildStatsLambdaUrl(stats2LambdaFunctionUrl); if (lambdaUrl) { const storageKey = buildStats2RequestKey( lambdaUrl, From f661648213e5920353736378fda2a4d520697690 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 10:43:33 -0600 Subject: [PATCH 04/11] update stats endpoint --- .../js_src/lib/tests/ajax/static/context/system_info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json index b5f069d5a0f..b328c94e680 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json @@ -3,7 +3,7 @@ "specify6_version": "6.8.03", "database_version": "6.8.03", "schema_version": "2.9", - "stats_url": "https://stats.specifycloud.org/capture", + "stats_url": "https://sp7-stats.specifycloud.org/capture", "database": "specify", "institution": "University of Kansas Biodiversity Institute", "institution_guid": "77ff1bff-af23-4647-b5d1-9d3c414fd003", From 28e465470117308ad5aeba359de82a4449b9ccce Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 12:16:12 -0600 Subject: [PATCH 05/11] test stats endpoint updates --- .../lib/components/InitialContext/__tests__/systemInfo.test.ts | 2 +- specifyweb/frontend/js_src/lib/tests/ajax/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts index 3162993cfee..40d3fec4695 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts @@ -15,6 +15,6 @@ test('can fetch and parse system information', async () => isa_number: '2014427', schema_version: '2.9', specify6_version: '6.8.03', - stats_url: 'https://stats.specifycloud.org/capture', + stats_url: 'https://sp7-stats.specifycloud.org/capture', version: '(debug)', })); diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts index 286814f5e8b..28013b71137 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts +++ b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts @@ -95,7 +95,7 @@ export async function ajaxMock( expectedErrors = [], }: Parameters[1] ): Promise> { - if (url.startsWith('https://stats.specifycloud.org/capture')) + if (url.startsWith('https://sp7-stats.specifycloud.org/capture')) return formatResponse('', accept, expectedErrors, undefined); const parsedUrl = new URL(url, globalThis?.location.origin); From 6147c0e3dc81e75bb1b4bf0aeda28c85b9d95298 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 13:40:50 -0600 Subject: [PATCH 06/11] skip lambda calls on local and test panel --- .../js_src/lib/components/InitialContext/stats.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index 084a00d5c16..c34994fba97 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -14,6 +14,7 @@ const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; const stats2RequestTimeoutMs = 5000; const stats2LambdaFunctionUrl = 'https://hvf3gvyu6q3f3mkf6y5xlddstq0xmuel.lambda-url.us-east-1.on.aws/'; +// const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; @@ -50,6 +51,14 @@ function recordStats2Request(storageKey: string, now = Date.now()): void { } catch {} } +function shouldSkipLambdaStatsRequest(hostname: string): boolean { + const normalizedHostname = hostname.toLowerCase(); + return ( + normalizedHostname === 'localhost' || + normalizedHostname.endsWith('.test.specifysystems.org') + ); +} + function pingInBackground(url: string): void { const controller = typeof globalThis.AbortController === 'function' @@ -119,6 +128,9 @@ export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { const lambdaUrl = buildStatsLambdaUrl(stats2LambdaFunctionUrl); if (lambdaUrl) { + if (shouldSkipLambdaStatsRequest(globalThis.location.hostname)) { + return; + } const storageKey = buildStats2RequestKey( lambdaUrl, `${systemInfo.collection_guid}` @@ -135,4 +147,5 @@ export const exportsForTests = { buildStats2RequestKey, shouldSendStats2Request, recordStats2Request, + shouldSkipLambdaStatsRequest, }; From e9f0a730ae6e793d36c279b260dc5901c5f81eb2 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 19:45:18 +0000 Subject: [PATCH 07/11] Lint code with ESLint and Prettier Triggered by 6147c0e3dc81e75bb1b4bf0aeda28c85b9d95298 on branch refs/heads/issue-7693 --- .../frontend/js_src/lib/components/InitialContext/stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index c34994fba97..cd06a74af82 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -14,7 +14,7 @@ const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; const stats2RequestTimeoutMs = 5000; const stats2LambdaFunctionUrl = 'https://hvf3gvyu6q3f3mkf6y5xlddstq0xmuel.lambda-url.us-east-1.on.aws/'; -// const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; +// Const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; From 61aaff5dce736955a1a3b40b05d5d888662f9d74 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 16:24:09 -0600 Subject: [PATCH 08/11] use new working static lambda url --- .../frontend/js_src/lib/components/InitialContext/stats.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index cd06a74af82..faaa1ed8bec 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -13,8 +13,7 @@ type StatsCounts = { const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; const stats2RequestTimeoutMs = 5000; -const stats2LambdaFunctionUrl = 'https://hvf3gvyu6q3f3mkf6y5xlddstq0xmuel.lambda-url.us-east-1.on.aws/'; -// Const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; +Const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; From 68c7a22c8d9689d2e3f0b23dee3a46ffeff46194 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 16:25:41 -0600 Subject: [PATCH 09/11] Update specify_settings.py --- specifyweb/settings/specify_settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/settings/specify_settings.py b/specifyweb/settings/specify_settings.py index afb5e548adc..becbc7dfd74 100644 --- a/specifyweb/settings/specify_settings.py +++ b/specifyweb/settings/specify_settings.py @@ -123,8 +123,7 @@ # 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" +STATS_2_URL = "https://stats-2.specifycloud.org" # Workbench uploader log directory. # Must exist and be writeable by the web server process. From 9d3baaadbdf37787d96c418affeb9cffa4a1ce34 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 17 Feb 2026 16:29:48 -0600 Subject: [PATCH 10/11] typo fix --- .../frontend/js_src/lib/components/InitialContext/stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index faaa1ed8bec..2e487b2128f 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -13,7 +13,7 @@ type StatsCounts = { const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; const stats2RequestTimeoutMs = 5000; -Const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; +const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; From 9251d66e9f2fceaa9ab9a6b4b46ce401e8271458 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 19 Feb 2026 15:55:40 -0600 Subject: [PATCH 11/11] Update specify_settings.py --- specifyweb/settings/specify_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/settings/specify_settings.py b/specifyweb/settings/specify_settings.py index becbc7dfd74..127874067f4 100644 --- a/specifyweb/settings/specify_settings.py +++ b/specifyweb/settings/specify_settings.py @@ -122,7 +122,7 @@ # Usage stats are transmitted to the following address. # Set to None to disable. -STATS_URL = "https://stats.specifycloud.org/capture" +STATS_URL = "https://sp7-stats.specifycloud.org/capture" STATS_2_URL = "https://stats-2.specifycloud.org" # Workbench uploader log directory.