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/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index 4bf4d164617..2e487b2128f 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -10,19 +10,72 @@ type StatsCounts = { readonly Specifyuser: number; }; +const stats2RequestIntervalMs = 24 * 60 * 60 * 1000; +const stats2RequestKeyPrefix = 'specify7-stats2-last-request'; +const stats2RequestTimeoutMs = 5000; +const stats2LambdaFunctionUrl = 'https://stats-2.specifycloud.org'; + function buildStatsLambdaUrl(base: string | null | undefined): string | null { if (!base) return null; let u = base.trim(); if (!/^https?:\/\//i.test(u)) u = `https://${u}`; + 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; - const hasRoute = /\/(prod|default)\/[^\s/]+/.test(u); - if (!hasRoute) { - const stage = 'prod'; - const route = 'AggrgatedSp7Stats'; - u = `${u.replace(/\/$/, '')}/${stage}/${route}`; + 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; } - return u; +} + +function recordStats2Request(storageKey: string, now = Date.now()): void { + if (globalThis.localStorage === undefined) return; + + try { + globalThis.localStorage.setItem(storageKey, `${now}`); + } 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' + ? 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) => { @@ -72,10 +125,26 @@ export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { { errorMode: 'silent' } ).catch(softFail); - const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); + const lambdaUrl = buildStatsLambdaUrl(stats2LambdaFunctionUrl); if (lambdaUrl) { - await ping(formatUrl(lambdaUrl, parameters, false), { - errorMode: 'silent', - }).catch(softFail); + if (shouldSkipLambdaStatsRequest(globalThis.location.hostname)) { + return; + } + const storageKey = buildStats2RequestKey( + lambdaUrl, + `${systemInfo.collection_guid}` + ); + if (!shouldSendStats2Request(storageKey)) { + return; + } + recordStats2Request(storageKey); + pingInBackground(formatUrl(lambdaUrl, parameters, false)); } }); + +export const exportsForTests = { + buildStats2RequestKey, + shouldSendStats2Request, + recordStats2Request, + shouldSkipLambdaStatsRequest, +}; 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); 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", diff --git a/specifyweb/settings/specify_settings.py b/specifyweb/settings/specify_settings.py index afb5e548adc..127874067f4 100644 --- a/specifyweb/settings/specify_settings.py +++ b/specifyweb/settings/specify_settings.py @@ -122,9 +122,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" +STATS_URL = "https://sp7-stats.specifycloud.org/capture" +STATS_2_URL = "https://stats-2.specifycloud.org" # Workbench uploader log directory. # Must exist and be writeable by the web server process.