-
Notifications
You must be signed in to change notification settings - Fork 30
Most Used APIs
- This is an outline to be updated *
https://github.com/zooniverse/front-end-monorepo/pull/3345
ERAS (Enhanced Running Average Stats Service) is Zooniverse's stats service for measuring volunteer effort and contribution. You can find comprehensive technical documentation in the ERAS Wiki.
The useStats() hook in lib-user provides a convenient way to fetch user and (user) group classification statistics with automatic caching via SWR (stale-while-revalidate).
Hypothetical use case: Suppose you want to fetch the classification statistics for a user, for a specific project, for all time, bucketed by day. You'd make a request like:
GET https://eras.zooniverse.org/classifications/users/12345?project_id=6789&period=dayThis would return the user with ID 12345's classification stats on project with ID 6789, broken down by days. The period=day parameter buckets the response by day, and returns stats for all time. To limit the time range to just 7 days, you'd add start_date and end_date like:
GET https://eras.zooniverse.org/classifications/users/12345?project_id=6789&period=day&start_date=2023-05-01&end_date=2023-05-08Using the useStats hook to make the request:
const { data: stats } = useStats({
endpoint: '/classifications/users',
sourceId: '12345',
query: {
project_id: '6789',
period: 'day',
start_date: '2023-05-01',
end_date: '2023-05-08'
}
})The hook handles authorization, environment detection (production vs. staging), and caching.
sourceId: The sourceId parameter can be either a user ID or a group ID—both are valid sources of classification statistics. The hook will only fetch data when sourceId is provided.
Real-world usage from UserStatsContainer.jsx:
import { useStats } from '@hooks'
const STATS_ENDPOINT = '/classifications/users'
// Fetch all projects stats
const { data: allProjectsStats, error: statsError, isLoading: statsLoading } = useStats({
endpoint: STATS_ENDPOINT,
sourceId: user?.id,
query: {
project_contributions: true,
time_spent: true
}
})
// Fetch individual project stats
const { data: projectStats } = useStats({
endpoint: STATS_ENDPOINT,
sourceId: user?.id,
query: {
project_id: selectedProject.id,
time_spent: true
}
})What // Fetch all projects stats returns:
- Includes
total_countfor the user's classifications across all projects in the requested date range. - Includes
time_spent(in seconds) becausetime_spent: trueis set. - Includes
project_contributionsbecauseproject_contributions: trueis set. This is an array showing per-project contribution counts (for example, each item includes aproject_idandcount).
Example response shape:
{
"total_count": 4123,
"time_spent": 3004,
"project_contributions": [
{ "project_id": 123, "count": 4000 },
{ "project_id": 234, "count": 123 }
]
}What // Fetch individual project stats returns:
- Includes
total_countfor only the selected project becauseproject_idis provided. - Includes
time_spent(in seconds) becausetime_spent: trueis set.
Example response shape:
{
"total_count": 250,
"time_spent": 890
}Key distinction: Use project_contributions: true when you want per-project breakdowns across all projects (returns an array). Use project_id: <specific_id> when you want to scope the query to a single project (returns aggregate stats for just that project).
The useYourProjectStats() hook in app-project provides project-specific classification statistics with two time periods: the last 7 days and all-time. This hook automatically calculates the appropriate date range by fetching the user's account creation date from Panoptes. These stats are used in the "Your Stats" section of the Classify page.
Example: Comparing recent and all-time project stats
In packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js:
const endpoint = '/classifications/users'
async function fetchStats({ endpoint, projectID, userID, token }) {
const host = statsHost(env) // Returns production or staging URL
const headers = { authorization: `Bearer ${token}` }
// Fetch user creation date to determine date range
const userCreatedAt = await fetchUserCreatedAt({ token, userID })
// 7 days stats
const sevenDaysResponse = await fetch(
`${host}${endpoint}/${userID}/?period=day&project_id=${projectID}`,
{ headers }
)
const sevenDaysStats = await sevenDaysResponse.json()
// All-time stats (from user creation date)
const allTimeResponse = await fetch(
`${host}${endpoint}/${userID}/?period=week&project_id=${projectID}`,
{ headers }
)
const allTimeStats = await allTimeResponse.json()
return { sevenDaysStats, allTimeStats }
}How these stats are displayed: In YourProjectStatsContainer.jsx, the container passes the fetched stats to the presentational component:
const { data, loading, error } = useYourProjectStats({ projectID, userID })
<YourProjectStats
data={data} // Contains sevenDaysStats and allTimeStats
loading={loading}
error={error}
/>The presentational component then displays these side by side:
<Stat
label={t('Classify.YourStats.lastSeven')}
value={data?.sevenDaysStats?.total_count}
/>
<Stat
label={t('Classify.YourStats.allTime')}
value={data?.allTimeStats?.total_count}
/>This shows volunteers their classification activity for the current project with a clear comparison of recent activity (last 7 days) and their all time contribution to that project.
Caesar is Zooniverse's aggregation service. FEM fetches reductions via GraphQL and renders them as consensus lines (transcription) or pre-populated marks (drawing).
For background on why this works the way it does, see ADR 60. For hook API docs, see the Hooks README. For test projects, see Benchmark Projects.
Workflow.caesarReducer → 'alice' | 'machineLearnt' | ''
↓
useCaesarReductions(key) → GraphQL fetch → Subject.setCaesarReductions()
↓ ↓
useTranscriptionReductions() useMachineLearntReductions()
↓ ↓
TranscribedLines.jsx DrawingToolMarksConnector.jsx
| Step | File | What to check |
|---|---|---|
| Reducer key | Workflow.js L53-61 |
Transcription: task type === 'transcription' → 'alice'. Drawing: enable_caesar_data_fetching === true + compatible tool type → 'machineLearnt'. Empty string → no fetch. |
| Config flag | WorkflowConfiguration.js |
enable_caesar_data_fetching (default false). Set via PFE project builder behind caesarDataFetching experimental flag. |
| GraphQL fetch | useCaesarReductions.js |
Fires on subject change. Errors go to Sentry + console. |
| Client | ClassifierContainer.jsx L25-34 |
Prod: caesar.zooniverse.org/graphql. Other: caesar-staging.zooniverse.org/graphql. |
| Storage | Subject.js L10, L25 |
CaesarReductions union type discriminated by reducer literal. caesarReductionsLoadedForStep prevents duplicate mark creation. |
| Transcription |
TranscriptionReductions.js → useTranscriptionReductions.js → TranscribedLines.jsx
|
consensusLines(frame) → SVG overlays. Visibility gated by shownMarks === ALL and lines.length > 0. |
| Drawing |
MachineLearntReductions.js → useMachineLearntReductions.js → DrawingToolMarksConnector.jsx
|
findCurrentTaskMarks({ stepKey }) → tool.createMark(). Mark shape must match reducedSubjectMocks.js. |
No data loading? Walk the chain:
-
classifierStore.workflows.active.caesarReducer— empty string means Caesar won't fetch. Check task type and config flag. - Network tab →
caesar.zooniverse.org/graphql— no request meanssubject?.idorreducerKeyis falsy. - Response empty → Caesar-side issue (extractors/reducers not configured). Test directly:
curl -X POST https://caesar.zooniverse.org/graphql -H 'Content-Type: application/json' -d '{"query":"{ workflow(id: ID) { subject_reductions(subjectId: ID, reducerKey: \"KEY\") { data } } }"}' -
classifierStore.subjects.active.caesarReductionsnull → race condition (subject changed before fetch resolved).
Data loading but marks wrong? Compare against reducedSubjectMocks.js for expected field shapes.
- Add tool type string to the
toolTypesarray inWorkflow.jsL103 - Add mock to
reducedSubjectMocks.js - Verify the tool's MST model accepts the fields Caesar outputs (may need field name mapping in the hook)
- Add test case to
MachineLearntReductions.spec.js
Currently only two paths exist (transcription + drawing). Mixed reducer types in one workflow are not supported. To add a third:
- New MST model with
reducer: types.literal('key')→ add to union inSubject.jsL10 - New condition in
Workflow.js::caesarReducer - New wrapper hook consuming
useCaesarReductions(key)→ wire into component