Skip to content

Most Used APIs

Mark Bouslog edited this page Mar 19, 2026 · 6 revisions
  • This is an outline to be updated *

APIs used in Zooniverse's frontend

Panoptes

Server-side vs Client-side

https://github.com/zooniverse/front-end-monorepo/pull/3345

ERAS

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.

Common Uses

User and Group Stats Pages (lib-user)

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=day

This 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-08

Using 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_count for the user's classifications across all projects in the requested date range.
  • Includes time_spent (in seconds) because time_spent: true is set.
  • Includes project_contributions because project_contributions: true is set. This is an array showing per-project contribution counts (for example, each item includes a project_id and count).

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_count for only the selected project because project_id is provided.
  • Includes time_spent (in seconds) because time_spent: true is 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).

Your Stats Section, Classify Page (app-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

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.

Data Flow

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.jsuseTranscriptionReductions.jsTranscribedLines.jsx consensusLines(frame) → SVG overlays. Visibility gated by shownMarks === ALL and lines.length > 0.
Drawing MachineLearntReductions.jsuseMachineLearntReductions.jsDrawingToolMarksConnector.jsx findCurrentTaskMarks({ stepKey })tool.createMark(). Mark shape must match reducedSubjectMocks.js.

Debugging

No data loading? Walk the chain:

  1. classifierStore.workflows.active.caesarReducer — empty string means Caesar won't fetch. Check task type and config flag.
  2. Network tab → caesar.zooniverse.org/graphql — no request means subject?.id or reducerKey is falsy.
  3. 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 } } }"}'
  4. classifierStore.subjects.active.caesarReductions null → race condition (subject changed before fetch resolved).

Data loading but marks wrong? Compare against reducedSubjectMocks.js for expected field shapes.

Adding a New Drawing Tool Type

  1. Add tool type string to the toolTypes array in Workflow.js L103
  2. Add mock to reducedSubjectMocks.js
  3. Verify the tool's MST model accepts the fields Caesar outputs (may need field name mapping in the hook)
  4. Add test case to MachineLearntReductions.spec.js

Adding a New Task Type

Currently only two paths exist (transcription + drawing). Mixed reducer types in one workflow are not supported. To add a third:

  1. New MST model with reducer: types.literal('key') → add to union in Subject.js L10
  2. New condition in Workflow.js :: caesarReducer
  3. New wrapper hook consuming useCaesarReductions(key) → wire into component

Sugar

Talk

Contentful

Zooniverse Blogs

Clone this wiki locally