Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: content releases #8454

Open
wants to merge 8 commits into
base: next
Choose a base branch
from
Open

feat: content releases #8454

wants to merge 8 commits into from

Conversation

bjoerge
Copy link
Member

@bjoerge bjoerge commented Jan 30, 2025

Description

This PR marks the public beta of the Content Releases feature. The content releases feature itself will be opt-in (for now), but these changes also introduces a few key improvements to the Studio UI, most notably the ability to switch between the published document and the current draft, and the ability to browse the Studio by only seeing what's currently published.

The Studio now supports a perspective search param, which is "sticky", meaning that it is preserved when navigating around in the studio. This param can take a comma-separated list of values (referred to as a "perspecive stack", which will then be forwarded to the perspective param used by list queries made by the Studio)

Terminology

With these changes we are introducing three new terms into the codebase, I'll cover them in brief here (there's more details to be found in internal documentation for the curious minded):

  • Version - This is comparable to a draft document but with a different prefix: instead of drafts.<publishedId> it will have versions.<releaseId>.<publishedId> . The versions. prefix is fixed, and no documents can have that as a prefix without following the versions.<string>.<id> structure.
  • Release – A release is represented by a system document that has an ID on the form _.release.<releaseId>. The <releaseId>-part here is what connects it with version documents. The <releaseId> part conventionally starts with r, and is followed by 8 random characters, e.g. rNe6H9RGZ.
  • Bundle - This is an internal, technical name used to connect documents in the release together. You can think of the releaseId as a type of bundle, but there could also be other types of bundles in the future.

There's no change to the semantics around draft documents, other than you can now think of them as version documents with special sematics applied to them.

What to review

This is a rather chonky one, when reviewing feel free to skp fast through things like tests and fixtures and focus on the more substantial stuff. Things in particular to watch out for are:

  • Changes to core APIs that could potentially be breaking
  • Changes to core that could affect data integrity in any way
  • Changes to datastores etc. that could pose performance issues

Note: we'll be using vX for various API calls if the feature is enabled, otherwise we'll stick with stable API versions. While it's not ideal to ship with vX, we have considered it to be an ok tradeoff in this case, given that the feature is opt-in and considered beta. Edit: Seems like this is not the case right now, we will be addressing this before merging, but adding a todo here:

  • Use vX only when opting-in to Content Releases

Testing

Most new code includes tests, but our coverage of existing code paths is still aspirational at best, so any manual testing is greatly appreciated.

Notes for release

tbd

Copy link

vercel bot commented Jan 30, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
page-building-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 31, 2025 4:57pm
performance-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 31, 2025 4:57pm
test-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 31, 2025 4:57pm
2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
studio-workshop ⬜️ Ignored (Inspect) Visit Preview Jan 31, 2025 4:57pm
test-next-studio ⬜️ Ignored (Inspect) Jan 31, 2025 4:57pm

Copy link
Contributor

No changes to documentation

Copy link
Contributor

github-actions bot commented Jan 30, 2025

Component Testing Report Updated Jan 31, 2025 12:03 PM (UTC)

❌ Failed Tests (1) -- expand for details
File Status Duration Passed Skipped Failed
comments/CommentInput.spec.tsx ✅ Passed (Inspect) 1m 6s 15 0 0
formBuilder/ArrayInput.spec.tsx ✅ Passed (Inspect) 12s 3 0 0
formBuilder/inputs/PortableText/Annotations.spec.tsx ❌ Failed (Inspect) 1m 20s 5 0 1
formBuilder/inputs/PortableText/copyPaste/CopyPaste.spec.tsx ✅ Passed (Inspect) 50s 11 7 0
formBuilder/inputs/PortableText/copyPaste/CopyPasteFields.spec.tsx ✅ Passed (Inspect) 0s 0 12 0
formBuilder/inputs/PortableText/Decorators.spec.tsx ✅ Passed (Inspect) 26s 6 0 0
formBuilder/inputs/PortableText/DisableFocusAndUnset.spec.tsx ✅ Passed (Inspect) 14s 3 0 0
formBuilder/inputs/PortableText/DragAndDrop.spec.tsx ✅ Passed (Inspect) 27s 6 0 0
formBuilder/inputs/PortableText/FocusTracking.spec.tsx ✅ Passed (Inspect) 1m 7s 15 0 0
formBuilder/inputs/PortableText/Input.spec.tsx ✅ Passed (Inspect) 1m 31s 21 0 0
formBuilder/inputs/PortableText/ObjectBlock.spec.tsx ✅ Passed (Inspect) 2m 3s 21 0 0
formBuilder/inputs/PortableText/PresenceCursors.spec.tsx ✅ Passed (Inspect) 13s 3 9 0
formBuilder/inputs/PortableText/Styles.spec.tsx ✅ Passed (Inspect) 26s 6 0 0
formBuilder/inputs/PortableText/Toolbar.spec.tsx ✅ Passed (Inspect) 1m 44s 21 0 0
formBuilder/tree-editing/TreeEditing.spec.tsx ✅ Passed (Inspect) 0s 0 3 0
formBuilder/tree-editing/TreeEditingNestedObjects.spec.tsx ✅ Passed (Inspect) 0s 0 3 0

Copy link
Contributor

github-actions bot commented Jan 30, 2025

⚡️ Editor Performance Report

Updated Fri, 31 Jan 2025 12:06:20 GMT

Benchmark reference
latency of sanity@latest
experiment
latency of this branch
Δ (%)
latency difference
article (title) 25.0 efps (40ms) 20.4 efps (49ms) +9ms (+22.5%) 🔴
article (body) 59.5 efps (17ms) 57.8 efps (17ms) +1ms (+3.0%)
article (string inside object) 25.0 efps (40ms) 24.4 efps (41ms) +1ms (+2.5%)
article (string inside array) 22.2 efps (45ms) 21.7 efps (46ms) +1ms (+2.2%)
recipe (name) 50.0 efps (20ms) 47.6 efps (21ms) +1ms (+5.0%)
recipe (description) 55.6 efps (18ms) 52.6 efps (19ms) +1ms (+5.6%)
recipe (instructions) 99.9+ efps (6ms) 99.9+ efps (6ms) +0ms (-/-%)

efps — editor "frames per second". The number of updates assumed to be possible within a second.

Derived from input latency. efps = 1000 / input_latency

Detailed information

🏠 Reference result

The performance result of sanity@latest

Benchmark latency p75 p90 p99 blocking time test duration
article (title) 40ms 44ms 70ms 425ms 784ms 10.7s
article (body) 17ms 20ms 31ms 301ms 344ms 6.3s
article (string inside object) 40ms 43ms 49ms 165ms 406ms 7.3s
article (string inside array) 45ms 47ms 55ms 74ms 478ms 7.5s
recipe (name) 20ms 21ms 22ms 41ms 0ms 7.0s
recipe (description) 18ms 19ms 21ms 29ms 0ms 4.5s
recipe (instructions) 6ms 8ms 9ms 28ms 0ms 3.2s

🧪 Experiment result

The performance result of this branch

Benchmark latency p75 p90 p99 blocking time test duration
article (title) 49ms 74ms 83ms 531ms 1262ms 11.9s
article (body) 17ms 20ms 28ms 223ms 367ms 6.5s
article (string inside object) 41ms 43ms 50ms 344ms 449ms 7.6s
article (string inside array) 46ms 50ms 55ms 92ms 446ms 7.6s
recipe (name) 21ms 23ms 29ms 43ms 0ms 7.1s
recipe (description) 19ms 20ms 23ms 33ms 0ms 4.6s
recipe (instructions) 6ms 8ms 9ms 14ms 0ms 3.2s

📚 Glossary

column definitions

  • benchmark — the name of the test, e.g. "article", followed by the label of the field being measured, e.g. "(title)".
  • latency — the time between when a key was pressed and when it was rendered. derived from a set of samples. the median (p50) is shown to show the most common latency.
  • p75 — the 75th percentile of the input latency in the test run. 75% of the sampled inputs in this benchmark were processed faster than this value. this provides insight into the upper range of typical performance.
  • p90 — the 90th percentile of the input latency in the test run. 90% of the sampled inputs were faster than this. this metric helps identify slower interactions that occurred less frequently during the benchmark.
  • p99 — the 99th percentile of the input latency in the test run. only 1% of sampled inputs were slower than this. this represents the worst-case scenarios encountered during the benchmark, useful for identifying potential performance outliers.
  • blocking time — the total time during which the main thread was blocked, preventing user input and UI updates. this metric helps identify performance bottlenecks that may cause the interface to feel unresponsive.
  • test duration — how long the test run took to complete.

@@ -35,39 +48,57 @@ export function getReferenceInfo(
documentPreviewStore: DocumentPreviewStore,
id: string,
referenceType: ReferenceSchemaType,
{version}: {version?: string} = {},
perspective: {bundleIds: string[]; bundleStack: PerspectiveStack} = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should consider renaming this to perspective: {ids: string[], stack: PerspectiveStack}

} else if (releaseId === 'published' || releaseId.startsWith('r')) {
perspectiveParam = releaseId
} else {
throw new Error(`Invalid releaseId: ${releaseId}`)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making a note for us to verify whether we can keep this hard validation of release ids, or whether we should rather consider it a convention

/**
* An array of all existing bundle ids.
*/
bundleIds: string[]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we rename this to ids?

* An array of release ids ordered chronologically to represent the state of documents at the
* given point in time.
*/
bundleStack: PerspectiveStack
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we rename this to just stack? not sure if perspective.bundleStack makes sense anymore?

* document readability for the version document
*/
version?: DocumentAvailability
// TODO: validate versions availability?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still relevant?


const QUERY_FILTER = `_type=="${RELEASE_DOCUMENT_TYPE}" && _id in path("${RELEASE_DOCUMENTS_PATH}.*")`

// TODO: Extend the projection with the fields needed
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still relevant?

*/
export interface ReleaseStore {
state$: Observable<ReleasesReducerState>
getMetadataStateForSlugs$: (slugs: string[]) => Observable<MetadataWrapper>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably fine to skip the $-suffix here and above (feels like a less valuable pattern combined with types)

/**
* @internal
*/
export function useReleaseOperations() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of creating a separate store per component it would be better if we moved this to a context and kept a single instance to it (the store is stateless, so it's probably not critical for merge)

* Gets all the releases ids
* @internal
*/
export function useReleasesIds(releases: ReleaseDocument[]): {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The double plural reads a bit awkward, better to rename it to useReleaseIds?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also consider whether we need it at all, seems to have somewhat limited value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming of this file seems a bit off to me. I think it's one of very few places we use "variant" as the common term for published, draft and versions. It might not be a 100% correct name, but could we consider renaming to getDocumentVersionType instead? At least that's closer to the name I would look for if I needed to know.

bjoerge and others added 3 commits January 30, 2025 23:01
Co-Authored-By: Jordan Lawrence <[email protected]>
Co-Authored-By: Bjørge Næss <[email protected]>
Co-Authored-By: Rita <[email protected]>
Co-Authored-By: pedrobonamin <[email protected]>
Co-Authored-By: Ash <[email protected]>
Co-Authored-By: Cody Olsen <[email protected]>
Co-Authored-By: Robin Neatherway <[email protected]>
Comment on lines +1 to +2
export * from '../store/_legacy'
export * from '../store/user'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this two exports from here

@@ -30,6 +30,7 @@ export type ReferenceFilterSearchOptions = {
tag?: string
maxFieldDepth?: number
strategy?: SearchStrategy
perspective?: string | string[]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the ClientPerspective type from @sanity/client here?

Comment on lines +82 to +92
/**
* This is a temporary hack to get around the fact that the `drafts.versions.versionName.id` format is not supported
* `versions.` don't have draft documents.
*
* Where is the drafts. prefix coming from and why it's added to the `versions.` documents?
* */
const cleanId =
data.documentId.includes('versions.') && isDraftId(data.documentId)
? data.documentId.replace('drafts.', '')
: data.documentId
const snapshot = await client.getDocument(cleanId, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should no longer be necessary

Suggested change
/**
* This is a temporary hack to get around the fact that the `drafts.versions.versionName.id` format is not supported
* `versions.` don't have draft documents.
*
* Where is the drafts. prefix coming from and why it's added to the `versions.` documents?
* */
const cleanId =
data.documentId.includes('versions.') && isDraftId(data.documentId)
? data.documentId.replace('drafts.', '')
: data.documentId
const snapshot = await client.getDocument(cleanId, {
const snapshot = await client.getDocument(data.documentId, {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be ideal if we reused all of these in @sanity/client/csm so we have a single source of truth:

import {
  type DraftId,
  DRAFTS_FOLDER,
  getDraftId,
  getPublishedId,
  getVersionFromId,
  getVersionId,
  isDraftId,
  isPublishedId,
  isVersionId,
  type PublishedId,
  VERSION_FOLDER,
} from '@sanity/client/csm'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants