From 618d0b130a960ac63e4a510f7a2110186cbb0e5a Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 18 Sep 2024 17:14:27 +0100 Subject: [PATCH] Added debug logging for 404 errors on new posts ref https://linear.app/tryghost/issue/ONC-323 - the post model state appears to be in an odd situation when this issue occurs, the extra log context should help us determine if the bad state is occurring at the route level or inside the editor controller --- ghost/admin/app/controllers/lexical-editor.js | 22 ++++++-- .../tests/unit/controllers/editor-test.js | 50 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 24e50cd9d4..e9b90f800c 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -184,6 +184,10 @@ export default class LexicalEditorController extends Controller { _saveOnLeavePerformed = false; _previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes + /* debug properties ------------------------------------------------------*/ + + _setPostState = null; + /* computed properties ---------------------------------------------------*/ @alias('model') @@ -658,8 +662,9 @@ export default class LexicalEditorController extends Controller { // it as saved and performing PUT requests with no id. We want to // be noisy about this early to avoid data loss if (isNotFoundError(error)) { - console.error(error); // eslint-disable-line no-console - Sentry.captureException(error, {tags: {savePostTask: true}}); + const context = this._getNotFoundErrorContext(); + console.error('saveTask failed with 404', context); // eslint-disable-line no-console + Sentry.captureException(error, {tags: {savePostTask: true}, context}); this._showErrorAlert(prevStatus, this.post.status, 'Editor has crashed. Please copy your content and start a new post.'); return; } @@ -681,6 +686,13 @@ export default class LexicalEditorController extends Controller { } } + _getNotFoundErrorContext() { + return { + setPostState: this._setPostState, + currentPostState: this.post.currentState.stateName + }; + } + @task *beforeSaveTask(options = {}) { if (this.post?.isDestroyed || this.post?.isDestroying) { @@ -702,7 +714,7 @@ export default class LexicalEditorController extends Controller { this.set('post.lexical', this.post.lexicalScratch || null); // Set a default title - if (!this.get('post.titleScratch').trim()) { + if (!this.post.titleScratch?.trim()) { this.set('post.titleScratch', DEFAULT_TITLE); } @@ -1042,6 +1054,8 @@ export default class LexicalEditorController extends Controller { // reset everything ready for a new post this.reset(); + this._setPostState = post.currentState.stateName; + this.set('post', post); this.backgroundLoaderTask.perform(); @@ -1211,6 +1225,8 @@ export default class LexicalEditorController extends Controller { this._leaveConfirmed = false; this._saveOnLeavePerformed = false; + this._setPostState = null; + this.set('post', null); this.set('hasDirtyAttributes', false); this.set('shouldFocusTitle', false); diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index 003de0507c..ed057afd11 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -1,5 +1,6 @@ import EmberObject from '@ember/object'; import RSVP from 'rsvp'; +import {authenticateSession} from 'ember-simple-auth/test-support'; import {defineProperty} from '@ember/object'; import {describe, it} from 'mocha'; import {expect} from 'chai'; @@ -424,4 +425,53 @@ describe('Unit: Controller: lexical-editor', function () { expect(isDirty).to.be.false; }); }); + + describe('post state debugging', function () { + let controller, store; + + beforeEach(async function () { + controller = this.owner.lookup('controller:lexical-editor'); + store = this.owner.lookup('service:store'); + + // avoid any unwanted network calls + const slugGenerator = this.owner.lookup('service:slug-generator'); + slugGenerator.generateSlug = async () => 'test-slug'; + + Object.defineProperty(controller, 'backgroundLoaderTask', { + get: () => ({perform: () => {}}) + }); + + // avoid waiting forever for authenticate modal + await authenticateSession(); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('should call _getNotFoundErrorContext() when hitting 404 during save', async function () { + const getErrorContextSpy = sinon.spy(controller, '_getNotFoundErrorContext'); + + const post = createPost(); + post.save = () => RSVP.reject(404); + + controller.set('post', post); + await controller.saveTask.perform(); // should not throw + + expect(getErrorContextSpy.calledOnce).to.be.true; + }); + + it('_getNotFoundErrorContext() includes setPost model state', async function () { + const newPost = store.createRecord('post'); + controller.setPost(newPost); + expect(controller._getNotFoundErrorContext().setPostState).to.equal('root.loaded.created.uncommitted'); + }); + + it('_getNotFoundErrorContext() includes current model state', async function () { + const newPost = store.createRecord('post'); + controller.setPost(newPost); + controller.post = {currentState: {stateName: 'this.is.a.test'}}; + expect(controller._getNotFoundErrorContext().currentPostState).to.equal('this.is.a.test'); + }); + }); });