Skip to content

Conversation

@melton-jason
Copy link
Contributor

@melton-jason melton-jason commented Dec 1, 2025

Currently, the sending of statistics to the statistics server happens as a part of the frontend fetching the general System Information:

if (systemInfo.stats_url !== null) {
let counts: StatsCounts | null = null;
try {
counts = await load<StatsCounts>(
'/context/stats_counts.json',
'application/json'
);
} catch {
// If counts fetch fails, proceed without them.
counts = null;
}
const parameters = {
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,
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
* them as they were sent in previous versions of Specify 7
*/
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) {
await ping(formatUrl(lambdaUrl, parameters, false), {
errorMode: 'silent',
}).catch(softFail);
}
}

Because there is data from the System Information that is required for the immediate functionality and behavior of some components, the endpoint was part of Specify's "Initial Context".

All endpoints which are considered essential (i.e., must be called for normal frontend operation), are a part of this Initial Context, which blocks access to the Specify application until all endpoints have been resolved.

export const initialContext = Promise.all([
// Fetch general context information (NOT CACHED)
import('../DataModel/schema'),
// Fetch data model (cached)
import('../DataModel/tables'),
// Fetch remote preferences (cached)
import('./remotePrefs'),
// Fetch icon definitions (cached)
import('./icons'),
// Fetch general system information (cached)
import('./systemInfo'),
// Fetch UI formatters (cached)
import('../FieldFormatters'),
// Fetch Specify 6 UI localization strings (cached)
import('./legacyUiLocalization'),
// Fetch user information (NOT CACHED)
import('./userInformation'),
// Fetch user permissions (NOT CACHED)
import('../Permissions'),
// Fetch the discipline's uniquenessRules (NOT CACHED)
import('../DataModel/uniquenessRules'),
]).then(async (modules) =>
Promise.all(modules.map(async ({ fetchContext }) => fetchContext))
);

/**
* - Load initial context
* - Display loading screen while loading
* - Display the main component afterward
*/
export function ContextLoader(): JSX.Element | null {
const [isContextLoaded = false] = useAsyncState(fetchContext, false);
const menuItems = useMenuItems();
const isLoaded = isContextLoaded && typeof menuItems === 'object';
/*
* Show loading screen only if didn't finish loading within 2 seconds.
* This prevents briefly flashing the loading dialog on fast systems.
*/
const showLoadingScreen = useDelay(!isLoaded, LOADING_TIMEOUT);
return isLoaded ? (
<Main menuItems={menuItems} />
) : showLoadingScreen ? (
<SplashScreen>
<h2 className="text-center">{commonText.loading()}</h2>
</SplashScreen>
) : null;
}

This caused problems when the Specify statistics server was not sending responses back to Specify clients, which resulted in the requests eventually timing out.
The entire time the Specify frontend was waiting on the statistics server to send a response, the user would be blocked from the application.

Because sending statistics is not vital to the application, I've implemented a "Secondary Context": a set of endpoints that Specify will call after fetching the initial context, and that will not block access to the frontend while being resolved.
I removed the sending of statistics from the fetching of the System Information and defined a new Promise on the frontend to handle sending statistics.

Checklist

  • Self-review the PR after opening it to make sure the changes look good and
    self-explanatory (or properly documented)
  • Add relevant issue to release milestone
  • Add pr to documentation list
  • Add automated tests

Testing instructions

I tested this by artificially injecting some delay in the resolution of the new statistics endpoint.

Screen.Recording.2025-12-01.at.10.42.18.AM.mov

You can use the code from the above video as a template in 1e6b59f. The stats endpoint should be now be called ~5 seconds after the stats promise is awaited.
When testing interference with Initial Context, you can uncomment the stats endpoint in the list of Initial Context endpoints:

  • Use the seconday-context-test branch
    • Alternatively, otherwise artificially slow down the resolution of the stats endpoint
  • Sign in to the Specify Instance
  • (Optionally) Ensure request caching is disabled in your browser (this can be toggled by the Disable cache checkbox in Chrome Dev Tools. Request caching will be disabled as long as Dev Tools is open)
  • Reload the page and ensure Specify is not affected by the slow statistics resolution

@melton-jason melton-jason requested a review from a team December 1, 2025 17:10
@github-project-automation github-project-automation bot moved this to 📋Back Log in General Tester Board Dec 1, 2025
@CarolineDenis CarolineDenis added this to the 7.12.0 milestone Dec 2, 2025
Previously, stats would block the application until it was
resolved. This led to a serious issue in which the stats
server was not responding and the request timed out: the
application would be completely inacessible while this was
occuring.
Because it's not essential, stats are now sent in
the background.

(cherry picked from commit f1904fe)
Triggered by f1904fe on branch refs/heads/secondary-context

(cherry picked from commit 3148768)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📋Back Log

Development

Successfully merging this pull request may close these issues.

4 participants