Overview
Requests to ApiLayoutController::post() that contain the same data as the saved entity will cause a auto-save entry to needlessly be be created
To confirm that data sent from the client that matches the saved entity will actually result in a auto-save entry you can test:
- Create a new article
- Change the title
- This results in change to the title and adds a "URL alias"
- Wait and see "Review 1 changes"
- Rename the page back to the original title and delete the "URL alias" which should not have existed
- Wait and see if "Review 1 changes" changes back to "No changes"
- It will not revert back even though the values have changed back
This because system does not recognize that the data being sent under entity_form_fields is effectively the same as the saved entity. See #6 for the details of why this is happening
This is better tested with Articles using xb_dev_standard test module than the Xb_page entity because there are more form widgets on the Article bundle.
This can also happen when front-end makes unneeded layout POST submit, see #3530068: When in Preview mode on published page an unneeded layout POST to the back end is made, that POST while not ideal should not result in new auto-saved entry
Proposed resolution
Determine if there is better system for comparing the current data to the "initial" state
Converting the auto-saved info to and entity first and then comparing against the save entity.
This entails making the auto-save manager only work with entities and has three additional benefits
- It makes #3524298: Auto-save data should update itself when the Fallback plugin is (de)activated redundant
- It gets us closer to workspaces support for content entities
- It avoids issues with the form state cache being having only 6 hrs of TTL whilst autosave has longer
Why this is worse than it seems!
While this more likely to happen in articles we are probably 1 form widget away from this being problem in xb_page
The testing instructions may seem kind of force but note this can also happen in this situation
- Editor opens an article in XB editor
- "Review 14 changes" appears at the top because they or others have made changes
- "Review 14 changes" may or may not change to other numbers while they are doing no action because other editors may be editing other entities
- The editor should be use to "Review X changes" , they would think nothing of it
- The editor accidently hits a character while any text box in "Page data", title or otherwise, has focus
- The editor realizes this and deletes the character or hits undo
- If this were a Xb_page then the "Published" badge would change to "Changed" and would not change back even though editor has undone their action
- Within the next 10 seconds, the "Review 14 changes"(or whatever the current number is, it might have changed) would change to "15"....probably but not for sure. If other editors are making changes or deleting other entities it could go more than 1, go down more than 1 or stay the same.
If the editor noticed the switch to "Changed" once we have #3529207: For selective reverting add DELETE auto-save endpoint and UI for it the editor could just revert the change. The problem is 1) the editor might not notice before they switch to another entity and 2) the editor would have to be sure the entity was "published" or they risk undoing other editors changes and there is not visual tool for them to determine if there are other changes.
If any other later opens this entity in XB there is no way for them to know it is actually the same as the save version even though it says "changed"
User interface changes
| Comment | File | Size | Author |
|---|---|---|---|
| #8 | initial-vs-save.png | 212.49 KB | tedbow |
| #2 | Screenshot 2025-06-11 at 5.55.42 PM.png | 164.91 KB | wim leers |
Issue fork experience_builder-3529622
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
wim leersReproduced.
Per the diagram in #3505118: The status badge should indicate if there are changes to the page, is shown when an auto-save is created.
And sure enough:
… is triggered because merely doing results in an auto-save entry:
… which explains it.
@tedbow: Can you please investigate when/why/how an auto-save entry gets created? 🙏
Comment #4
tedbowNot sure why this happens but appears but for some reason when viewing the preview that the auto-save is triggered.
I pushed up a debug branch I can see by retrieving /xb/api/v0/auto-saves/pending the auto-save is made during the preview before the exit.
I can also see in the call to
\Drupal\experience_builder\AutoSave\AutoSaveManager::recordInitialClientSideRepresentationwhich is used to save a comparison to determine if an auto-save should be made `form_build_id` and other form fields are missing. this could be the problem. Have idea to fix itComment #6
tedbowI think 2 things are going on
\Drupal\experience_builder\AutoSave\AutoSaveManager::save()fromApiLayoutController, at least for the main entity that hasentity_form_fieldswill always result in a new auto-save entity. Here is why\Drupal\experience_builder\Controller\ApiLayoutController::getwe have the call to record the initial has for the entity.Here is example result
Notice here we don't have
form_idorform_*fieldsentity_form_fieldsis set via$entity_form_fields = $this->getEntityData($entity);\Drupal\experience_builder\Controller\ApiLayoutController::buildPreviewRenderableIf we look at the actual auto-save entity we have
Notice
form_build_idis underentity_form_fieldsSo because we compare the has made from
recordInitialClientSideRepresentation()without the form fields we should always get a new auto-save entry because they will never match.so the question would be why don't we always get an auto-save entry when there is not changes. I think this is because we normally don't trigger a call to
\Drupal\experience_builder\AutoSave\AutoSaveManager::saveif there are no actual changes. The call tosave()should only add a entry if there are changes.To confirm this you can do
The auto-save entry is
The values are back to the "initial state" except now we have
form_build_id,form_tokenetcComment #7
tedbowhmm. actually I didn't notice
\Drupal\experience_builder\AutoSave\AutoSaveManager::generateHashhaswhich I think I added. So it must be some other key that is different. looking....
Comment #8
tedbowI think the solution I pushed should fix this for xb_pages, both problem initially reported and the problem I describe in #6 where you change the title of page and then change it back but the "Review 1 change" does not go away
but it does not fix it for articles using the
xb_dev_standardmodule. This is because the article have more properties that are exposed inentity_form_fields. So this just happens to fix it for xb_pages but if we say added the menu property and widget to pages this would break it again.Maybe the problem is the logic in
\Drupal\experience_builder\Controller\ApiLayoutController::getEntityDataand\Drupal\experience_builder\ClientDataToEntityConverter::setEntityFields()are too differentHere is a comparison between the data used to make the hashes between the initial and then call to save an updated.

Comment #9
larowlanAh, that's because the form state gets set from input behaviours in the FE which does things like mapping booleans from 0/1 to true/false
Tricky 🤔
Comment #10
larowlanI wonder if we can track why we're posting to the layout endpoint - the preview comes back from the get controller now so we shouldn't be making any post requests _until_ we change something.
Comment #11
tedbow@larowlan re #10 I agree we shouldn't need to make post requests until something changes but also we should be able to make post requests and not create an auto-save entries if the form represents that same data as the save entity. Extra posts requests should not matter in that regard.
We should be able to change the form to something different to what is saved, and then change the form back to what is saved in the entity and then that should delete auto-save entry because the system detects the match.
The extra post requests seem like a minor problem but the inability to detect when a state matches the saved entity seems critical
re #9
I can't remember if we consider not relying on the form state but instead relying what would be saved if we published. Would it be possible to run
\Drupal\experience_builder\ClientDataToEntityConverter::convert()on auto-saved entry, like we do in\Drupal\experience_builder\Controller\ApiAutoSaveController::post()and then compare against the saved entity?Something like
I know we there would be more to it than this and we probably have massage things, but also the form values method also seem to need a lot of massaging and seems really tricky
Just trying to think of alternatives.
Comment #12
tedbowre #11 another way I am trying to think about the problem is imaging if we had been able to use Workspaces instead of I current auto-save.
Just considering Content entities., if we had workspaces and were able to make real entity revisions in the workspaces would we have the problem of not being able to detect when the revisions in the workspace are actually the same as the revision in the live site?
Not considering XB, in standard workspaces if you edited the title of node in a workspace from "Original title" to "Updated title" back to "Original title". Not sure?
Comment #13
tedbowwill update summary too
Comment #14
tedbowI created #3530068: When in Preview mode on published page an unneeded layout POST to the back end is made to handle the specific problem. This issue will handle problem of creating a new auto-save entry because the system does not recognize it is the same as the initial state
Comment #15
tedbowUpdated summary. I changed it to major but I think it is really a product decision as to whether this acceptable for beta1 or 1.0. I definitely want surface it because I don't think we aware this problem still exists.
Unlike the data model, I don't think this likely something we couldn't fix later if the behavior is acceptable for now.
Re #12 and how this kind of problem compares to something like Workspaces that allows staging of content
I don't think Workspaces would detect if you changed an entity in workspace but then changed it back so it matched the original. So you would see "changed" entity that could be published to Live.
I think the difference you would have to submit a form at least once for this to happen, where as XB auto-save on keystrokes, so I think the editor would have much harder time figuring out what happen for unintentional changes where just want to "view" an entity XB. Even if you don't want to change a page if just want to see what the page would look like if say a JS component it uses that has changed, you have to open it up in the editor. There is no other way to just "View"
Comment #16
larowlanBecause its Ted's weekend I'll try and push this forward a bit
Comment #18
larowlanI've started a new branch named
3529622-convert-before-autosavethat changes the signatures on autosave to only take the entity, i.e. you have to convert before you ask the autosave manager to save it.I've also done some work to make hashing of the entity determinate and have APiLayoutControllerPostTest::post passing. I expect there will be a lot of failure and I will check in with @tedbow when I start tomorrow to get a handover to pick it back up again. No pressure to work on this @tedbow
Comment #19
tedbowI pushed this a little more forward I hope.
Comment #20
larowlanUpdating the issue title to reflect the approach.
Comment #21
larowlanAdding beta-blocker tag as this blocks #3524298: Auto-save data should update itself when the Fallback plugin is (de)activated and that itself has the tag
Comment #22
effulgentsia commentedComment #23
larowlanShould be down to just failures in ApiConfigAutoSaveControllersTest now, will continue next week
Comment #24
larowlanIt was a journey, but this is now green and ready for review.
I think this makes our auto-save much more robust and reliable.
It makes it clear that we probably want to do #3505018: Return validation errors from LayoutController::post and ::patch sooner than later too
More accurate issue title
Comment #25
larowlanComment #26
larowlanPostponed #3514900: EntityFormController should load auto-save state if it exists on top of this. It is critical because of data-loss, so that makes this critical too.
Comment #27
wim leersThis blocks #3527244: Add test coverage that ensure fields values the user has view but not edit access to can NOT be saved to AutoSave and will likely allow us to close #3509267: [PP-1] Only auto-save JavaScriptComponent/AssetLibrary config entities when there are actual changes and #3525829: If user with less/different permissions edits a page after a user with more/different permissions data loss could occur.
Comment #29
wim leersClosed #3509267 as a duplicate at #3509267-6: [PP-1] Only auto-save JavaScriptComponent/AssetLibrary config entities when there are actual changes.
Crediting @justafish for her clarifying response at https://git.drupalcode.org/project/experience_builder/-/merge_requests/1..., important given the recent turmoil at #3532130: `/xb/api/v0/config/component` crashes when `article` node type does not exist due to hardcoded assumption in `GeneratedFieldExplicitInputUxComponentSourceBase::getClientSideInfo()` 🙏
Comment #31
tedbowExciting to see this approached looks like it is going to work out! ➕1 from me. Going look at the auto-save portions
Comment #32
tedbowI think this looks good but I don't we have actual test coverage for the problem in #6, Where you change the title and change it back to original and the system can't detect that current state matches the save entity state. If you look at #8 you can see that form values are different, like in one case "menu" is include and in the other it is not. Since we not rely on the form values to detect if we to save or clear the auto-save this should fix it but in a follow-up it would be good to write an e2e test to prove the current entity would be removed from the "review x changes". You can check the summary where I put "Why this is worse than it seems!" section that details how this could be bad. Also the instructions in the summary under
Shows how to manually test this. So it might be good start for writing a test.
but for this issue I have manually tested the MR and it does fix the problem!
Comment #33
wim leers@tedbow in #32: I believe between https://git.drupalcode.org/project/experience_builder/-/merge_requests/1... and the few lines I added at https://git.drupalcode.org/project/experience_builder/-/merge_requests/1..., there is the necessary test coverage already. You're right though that this does not necessarily reflect all the possible such bool-as-int, int-as-string etc scenarios. But I'm not sure an e2e test could ever test all of those scenarios anyway, since we won't be able to test with every possible widget and every possible input for each widget.
Comment #34
wim leersI did some tightening of the "collapse/uncollapse" logic because I was worried the duplication of complex logic would make XB overall less reliable/harder to maintain.
In doing so, I uncovered a long-existing bug … which I first tried to fix in here, but it requires careful analysis, so: #3532414: Follow-up for #3500386: tighten `::collapse()` to improve data integrity, because new props added to auto-saved code components cannot have a widget anyway.
Comment #35
wim leersThis is a HUGE, EPIC MR. It hardens countless things and fixes a number of bugs, some recent, some old. Impressive! 👏
That also means this took >5 hours to review 😅, because since this affects data integrity (due to the introduction of
ComponentSourceInterface::preSaveItem()etc.), I wanted to be absolutely certain I understood all those pieces. Which explains #34.I'm less concerned about auto-save, because that can't affect data integrity — losing auto-saves ain't the end of the world, corrupted component trees on live sites is.
Given this blocks so many things and clearly improves the status quo: RTBC! But I do see a number of things that need either a follow-up issue (or MR here) or that aren't fully clear to me yet:
preSaveItem(ComponentTreeItem $item)should beoptimizeExplicitInput(array $inputValues)before Component Source plugins become a public APIComment #37
wim leersComment #38
larowlanFollow ups all ready for review
Comment #39
wim leersThis passes tests on SQLite & MariaDB, but introduced a regression on MySQL & PostgreSQL. 🫠
+
— MySQL 8
Same on PostgreSQL 16
Comment #40
wim leers#38: merged almost all of those! 🚢🚢🚢🚢 Except #3532495: Simplify ApiAutoSaveController::post for page regions, which contained a bit I didn't understand, so I made adjustments and approved, awaiting you or @penyaskito to +1 or not 🙏
Comment #42
larowlanFixes in new MR
Comment #44
wim leersThanks!