Skip to content

feat(server): add PATCH /projects/{project_identifier}/retention - #15410

Open
sahildayal wants to merge 3 commits into
Arize-ai:mainfrom
sahildayal:feat/project-retention-rest-12268
Open

feat(server): add PATCH /projects/{project_identifier}/retention#15410
sahildayal wants to merge 3 commits into
Arize-ai:mainfrom
sahildayal:feat/project-retention-rest-12268

Conversation

@sahildayal

@sahildayal sahildayal commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes #12268

Summary

Adds PATCH /v1/projects/{project_identifier}/retention — set which trace-retention policy a project uses, or reset it to the default.

Retention policies are standalone, reusable entities (ProjectTraceRetentionPolicy) that projects point at via Project.trace_retention_policy_id. This endpoint only changes that pointer: it never creates, edits, or deletes a policy. Policy CRUD stays with the existing GraphQL mutations, per the issue's recommendation to start with reference-by-id only.

API shape

// PATCH /v1/projects/my-project/retention
{ "policy_id": "<ProjectTraceRetentionPolicy GlobalID>" }   // assign
{ "policy_id": null }                                       // reset to the default policy
// 200
{ "data": { "project_id": "<Project GlobalID>", "policy_id": "<GlobalID or null>" } }

policy_id is required but nullable. That is deliberate: with an optional field, "omitted" and "explicitly null" are indistinguishable in Pydantic, and one of those means reset this project's retention. Making it required means a malformed call is a 422 rather than a silent reset. There's a test pinning that.

null maps to trace_retention_policy_id = NULL, which the schema already defines as "use the default policy" — so the response echoes null rather than inventing the default policy's ID.

Errors

  • 422 — malformed policy_id, or the field omitted entirely
  • 404 — well-formed policy ID that doesn't resolve, or unknown project
  • Validation happens before the write, so a failed call leaves the assignment untouched (tested).

Permissions

require_admin + is_not_locked, matching the ADMIN requirement in the issue and the existing update_project / delete_project routes in this router. The underlying GraphQL mutations carry IsAdminIfAuthEnabled as well.

Tests

Nine tests in TestSetProjectRetentionPolicy (tests/unit/server/api/routers/v1/test_projects.py), all passing on SQLite:

  • assigns a policy and persists it
  • accepts a project GlobalID as the identifier
  • null resets to the default policy
  • reassigns between two policies
  • assignment does not mutate the policy itself — policies are shared, so this guards against a future refactor turning assign into edit
  • 404 unknown policy, and the project's assignment is unchanged
  • 404 unknown project
  • 422 malformed policy_id
  • 422 when policy_id is omitted

Also registered the route in _ADMIN_ONLY_ENDPOINTS in tests/integration/_helpers.py — the coverage assert runs at import time, so a missing entry fails the whole integration suite.

I could not run the Postgres matrix locally (no local Postgres/Docker), so that path is covered only by CI here.

Open question from the issue

The issue asks whether this endpoint should also accept an inline policy definition ({"cron_expression": ..., "rule": {...}}) that creates and attaches a policy in one call. I followed the issue's own recommendation and implemented reference-by-id only. Happy to add inline creation if you'd like it, though it does seem like it belongs with policy CRUD rather than here.

Note on overlap

This touches projects.py, as does #15409 (POST /projects/{id}/clear, for #12267). The two add separate handlers in different parts of the file so they should merge independently, but I'm happy to rebase whichever lands second.

Retention policies are standalone, reusable entities that projects point
at via Project.trace_retention_policy_id. This endpoint sets which policy
a project uses; it never creates, edits, or deletes a policy, so policy
CRUD stays with the existing GraphQL mutations.

policy_id is required but nullable. An optional field cannot distinguish
an omitted key from an explicit null, and one of those means "reset this
project's retention" — so requiring the field turns a malformed call into
a 422 rather than a silent reset. Null maps to a NULL column, which the
schema already defines as "use the default policy".

Validation runs before the write: a malformed policy ID is a 422 and an
unresolvable one a 404, leaving the existing assignment untouched.

Guarded by require_admin and is_not_locked, matching the sibling project
routes and the IsAdminIfAuthEnabled on the policy mutations.

Closes Arize-ai#12268

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sahildayal
sahildayal requested review from a team as code owners August 12, 2026 18:47
@github-project-automation github-project-automation Bot moved this to 📘 Todo in phoenix Aug 12, 2026
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 12, 2026
@github-actions github-actions Bot added the triage issues that need triage label Aug 12, 2026
sahildayal and others added 2 commits August 14, 2026 23:52
Picks up the js workspace move (Arize-ai#15394), which relocated app/ to js/app/.
No source conflicts; the generated TS client change follows the rename.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files. triage issues that need triage

Projects

Status: 📘 Todo

Development

Successfully merging this pull request may close these issues.

REST API: PATCH /projects/{project_identifier}/retention — set trace retention policy

2 participants