Repository navigation
Add performance report methods - #2078
Merged
Merged
Conversation
The numbers on a project's Performance page and on the workspace Monitor are now available over REST (/api/v1/performance). This adds the SDK methods that read them: client.get_performance_metrics() client.get_workspace_performance_metric(metric, start_date, end_date) client.get_workspace_labeler_performance(start_date, end_date) project.get_performance_metric(metric, start_date, end_date) project.get_labeler_performance(start_date, end_date) project.get_reviewer_performance(start_date, end_date) project.get_performance_report_download(start_date, end_date) Results are typed models (PerformanceMetricSeries, ProjectLabelerPerformance and so on) with snake_case attributes. The two project tables come back as PerformanceRows, which fetches a page at a time while iterating and exposes the table's total. Dates can be a date, a datetime or a "2026-01-31" string and are sent as UTC days, which is how the reports bucket. ID filters take IDs or SDK objects. API errors are raised as the SDK's own exceptions, and a refused query names the parameters at fault. Project.labeler_performance() is unchanged; it is a different, older report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
manuaero
requested review from
KeshavSahoo,
cyrusj89,
gramos-lb,
john-labelbox,
labelbox-engineering,
lgluszek and
snmodi21
October 10, 2026 09:14
The performance reports return agreement scores and rework and approval rates as fractions (0.9 for 90%). Say so on every class that carries one, and use realistic values in the test rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filters are documented to take IDs or SDK objects, but only objects with a `uid` were understood. UserGroup is a pydantic model that carries its ID as `id`, so passing groups to `user_group_ids` failed while joining the list. Read `uid`, then `id`, and refuse a value that has neither with a message that says what was passed, before any request is made. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the performance REST API: - Labeler and reviewer rows carry `member_type`: whether the member belongs to your organization (`internal`) or to another one, such as a workforce provider (`external`). `PerformanceMemberType` names the two. - `get_workspace_performance_metric` and `get_workspace_labeler_performance` take `member_type` and `owner_organization_ids`, the Monitor's Member Type and "owned by" filters. - `get_workspace_labeler_performance` returns `PerformanceRows`, fetched a page at a time like the project tables, with `sort_by`, `descending`, `page_size` and `use_cache`. It returned the whole table as a list. - A failure of the service (5xx) or a timeout is tried again on the schedule `Client.execute` uses. When it stops trying, the error raised is the failure itself, not a retry error. A timeout is raised as `lbox.exceptions.TimeoutError` and a connection failure as `NetworkError`, as elsewhere in the SDK. A refused request (4xx) is not repeated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ca0d0e0. Configure here.
A request waits up to 120 seconds, and the retry schedule's deadline, which counts from the first attempt, was the library's default of 120 seconds. So by the time a request timed out the deadline had passed, and it was never tried again: only quick failures were. The deadline is now twice the request timeout: one more attempt after a timeout, and no third. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- When retries run out, the failure is raised without being chained back to the retry error. The retry error already names the failure as its cause, so `raise cause from error` made the two point at each other, and traceback printers that follow the chain (rich's among them) never finish. - An empty list for a filter (`user_ids=[]`) raises `ValueError` instead of leaving the filter off. It is nearly always a list that came out empty, and leaving the filter off reported on everyone. `None` still means no filter. - Iterating `PerformanceRows` a second time reads the table again from its first page. The first page is kept so that reading `total` and then the rows costs one request; a second pass started from that old page and continued with newly fetched ones. - `sort_by` is turned into the API's spelling here instead of by pydantic's `to_camel`, which on older 2.x releases lower-cases a name that is already in camel case. - Project tables no longer document sorting by email; the API does not offer it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the API after it was tested against production: the performance service refuses a page of more than 100 rows, and the score histograms' ranges are now given as fractions from 0 to 1, like the scores themselves. Docstrings only; the default page size (50) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workspace table keeps the rows of a project that can no longer be found, and the API now returns null for its name and owner where it returned the text "N/A". The three attributes are optional. Also says that an Alignerr account is external to everyone, which testing against production showed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A workspace metric narrowed by owning organization now says, in `skipped_project_ids`, which of those owners' projects it does not count because the caller may not view everyone's performance in them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `unit-data` and `integration` jobs have failed on develop, and so on
every pull request, for five separate reasons.
MEA prediction import is gone from the backend. The prediction import
service rejects every import of that type ("9 FAILED_PRECONDITION: MEA
prediction import has been deprecated"), so the nine tests that call
`ModelRun.add_predictions()` are skipped, with that reason. The tenth test
in the file only reads a model run; it passes and stays enabled.
The embedding fixture could not heal itself. The staging organization is
at the cap of 10 custom embeddings. The fixture sweeps stale ones and
retries when it hits the cap, but only inside `except LabelboxError`, and
since `Client.create_embedding` became a GraphQL mutation the cap arrives
as `MalformedQueryException`, which is not a `LabelboxError`. So the sweep
never ran, and every test that needs an embedding errored at setup: three
in test_embedding.py, test_export_embeddings_custom and test_mmc_all. The
helper now catches both, for the create and for the delete of a stale
embedding that another job has already removed.
Validation messages are masked. Deployed API environments replace the
message of every GraphQL validation error with "GraphQL validation error".
`test_semantic_error` and `test_query_complexity_error` expected the
detailed text; they now accept the masked message, or the detailed one from
an API that does not hide schema details. Nothing else is accepted.
Two tests raced the backend. A global key is not free the moment its data
row is deleted (the import checks keys against the search index, which
drops the row a little later), and three labels are not listed the moment
they are created. Both tests now wait for the condition they depend on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 tasks done
A newer formatter than the one CI runs had restyled one statement that this change does not touch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both failed once in the first run with the other fixes in place. `test_update_project_resource_tags` compared the number of resource tags in the organization before and after creating one (346 > 347). The suite runs for five Python versions at once against one organization, creating and deleting tags, so the count can go either way. It now looks for the two tags it created. `test_model_run_project_labels` read the first label of each data row from a model run export taken straight after `upsert_labels`. The export catches up a little later, and until then a data row comes out with no label for the run (IndexError). It now exports again while any is missing, for up to three minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Client.is_feature_schema_archived` raised "Failed to get the feature schema archived status." for any answer other than 200 or 404, without the status or the body. `test_feature_schema_is_archived` failed that way once against staging and the log could not say why (a rate limit, or a server error). The message now carries both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
Merged
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
The numbers on a project's Performance page and on the workspace Monitor are being made available over REST, under
/api/v1/performance(Labelbox/intelligence#31542). This adds the SDK methods that read them. No ticket.What a caller gets:
PerformanceMetricSeries(withPerformanceTimeBucketorPerformanceScoreBucketbuckets),ProjectLabelerPerformance,ProjectReviewerPerformance,WorkspaceLabelerPerformance,PerformanceReportDownload. Fields the API adds later are ignored, so an older SDK keeps working.PerformanceRows: iterate it and it fetches a page at a time;.totaland.cached_atread the first page once. Nothing is requested until it is read.date, adatetimeor a"2026-01-31"string and are sent as UTC days, which is how the reports bucket. Filters (user_ids,user_group_ids,batch_ids,project_ids) take IDs or SDK objects, whether the object carries its ID asuid(most of the SDK) or asid(UserGroup). A value with no ID raisesTypeErrorbefore any request is made.intervalanddeleted_labelstake the enums or plain strings.InvalidQueryErrornaming each parameter the API refused, 401 →AuthenticationError, 403 →AuthorizationError, 404 →ResourceNotFoundError, 429 →ApiLimitError, 5xx →InternalServerError.Implementation notes:
labelbox/schema/performance.py; the methods onClientandProjectare thin wrappers. It uses the client's existingconnectionandrest_endpoint, like the other REST-backed methods.Project.labeler_performance()is unchanged. It is a different, older report with no period; the new method's docstring says so.docs/labelbox/performance.rst, listed in the index. The user-facing guide is Labelbox/lbx-docs-mintlify#295.Do not release before the API is live. These methods call endpoints that exist only once Labelbox/intelligence#31542 is deployed. Until then they raise
ResourceNotFoundError.Type of change
All Submissions
New Feature Submissions
Changes to Core Features
What was run
pytest tests/unit/schema/test_performance.py: 46 tests, all passing, plustests/unit/test_client.pyandtests/unit/test_project.py(63 in total). They mock the HTTP session and check the exact URL and query string sent, the parsed results, paging, and the error mapping. Response fixtures are the shapes asserted by the API's own tests.ruff format --checkclean;ruff checkclean under the repo's rule set;mypyclean onperformance.py,client.pyandproject.py.What was not run
ryeis not installed on the machine this was written on, so the checks above were run directly withpytest,ruffandmypyin a Python 3.12 environment, not throughrye run.Added since this PR was opened
Follows the API changes in Labelbox/intelligence#31542.
Whose member did the work. Labeler and reviewer rows carry
member_type:"internal"for a member of your organization,"external"for a member of another one, such as a workforce provider.PerformanceMemberTypenames the two values.The Monitor's Member Type and "owned by" filters.
get_workspace_performance_metricandget_workspace_labeler_performancetakemember_typeandowner_organization_ids.The workspace table is paged.
get_workspace_labeler_performancenow returnsPerformanceRows, fetched a page at a time like the project tables, withsort_by,descending,page_sizeanduse_cache. It returned the whole table as a list. Nothing was released with the old shape.Retries. A failure of the service (5xx) or a timeout is tried again on the schedule
Client.executeuses (google.api_core.retry). When it stops trying, the error raised is the failure itself (InternalServerError,TimeoutError), not a retry error. A connection failure is raised asNetworkError, as elsewhere in the SDK. A refused request (4xx, including 429) is not repeated. The retry deadline is twice the 120-second request timeout, so a request that timed out is tried once more; under the library's default deadline (also 120 seconds) it never was, which Bugbot caught.An empty filter is refused.
user_ids=[](or any other ID list) raisesValueErrorbefore anything is sent. It was treated as "no filter" and reported on everyone.Nonestill means no filter.Errors do not point back at each other. When retries ran out, the failure was raised
fromthe retry error, which already named it as its cause. Traceback printers that follow the chain (rich's among them) never finished. It is now raised outside the handler.A second pass over
PerformanceRowsreads the table again. It started from the first page kept from the first pass and continued with newly fetched pages.sort_byis spelled for the API here, not by pydantic'sto_camel, which on older 2.x releases lower-cases a name already in camel case.Sorting the project tables by email is no longer documented; the API does not offer it.
The last five came out of an independent review of the module.
Tests: 89 in
tests/unit/schema/test_performance.py(106 withtest_client.pyandtest_project.py), run with the response shapes the API returns. Still no integration tests: the endpoints are not on staging yet.CI
The
unit-dataandintegrationjobs fail ondevelopitself, on tests this change does not touch. Those are fixed in #2079, which is merged into this branch so that both can be checked together. If #2079 lands first, this pull request's diff shrinks back to the performance module.Also added since this was opened, after testing against production: a page takes at most 100 rows; score ranges are fractions; a project that is gone has no name or owner;
skipped_project_idson a workspace metric split by owner.🤖 Generated with Claude Code
Note
Medium Risk
Large new read-only API surface with org- and project-level performance permissions and paging/retry behavior; methods will 404 until the backing REST API is deployed.
Overview
Adds SDK support for Performance page and workspace Monitor data via new
/api/v1/performanceREST calls, implemented inlabelbox.schema.performancewith thin wrappers onClientandProject.Callers can list metrics, fetch time/score series with filters (users, groups, batches, deleted labels, intervals), page labeler/reviewer tables through
PerformanceRows, download project report files, and query workspace-wide metrics and labeler tables (includingmember_typeandowner_organization_ids). Responses are typed Pydantic models (exported from the package), with URL-safe path segments, 120s timeouts, transient 5xx/timeout retries, strict rejection of empty ID filters, and HTTP status mapped to SDK exceptions.Documentation adds
docs/labelbox/performance.rstto the Sphinx index.Clientalso improves the ontology feature-schema error message with status code and body text.Test/CI hardening bundled in the same branch: skip deprecated MEA prediction-import integration tests; retry model-run export until labels appear; embedding cap healing treats
MalformedQueryException; integration tests tolerate masked GraphQL validation errors, global-key index lag after delete, label listing delay, and flaky org resource-tag counts.Reviewed by Cursor Bugbot for commit 7d62edc. Bugbot is set up for automated code reviews on this repo. Configure here.