0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

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
This commit is contained in:
Kevin Ansfield 2024-09-18 17:14:27 +01:00
parent 5923588818
commit 618d0b130a
2 changed files with 69 additions and 3 deletions

View file

@ -184,6 +184,10 @@ export default class LexicalEditorController extends Controller {
_saveOnLeavePerformed = false; _saveOnLeavePerformed = false;
_previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes _previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes
/* debug properties ------------------------------------------------------*/
_setPostState = null;
/* computed properties ---------------------------------------------------*/ /* computed properties ---------------------------------------------------*/
@alias('model') @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 // it as saved and performing PUT requests with no id. We want to
// be noisy about this early to avoid data loss // be noisy about this early to avoid data loss
if (isNotFoundError(error)) { if (isNotFoundError(error)) {
console.error(error); // eslint-disable-line no-console const context = this._getNotFoundErrorContext();
Sentry.captureException(error, {tags: {savePostTask: true}}); 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.'); this._showErrorAlert(prevStatus, this.post.status, 'Editor has crashed. Please copy your content and start a new post.');
return; return;
} }
@ -681,6 +686,13 @@ export default class LexicalEditorController extends Controller {
} }
} }
_getNotFoundErrorContext() {
return {
setPostState: this._setPostState,
currentPostState: this.post.currentState.stateName
};
}
@task @task
*beforeSaveTask(options = {}) { *beforeSaveTask(options = {}) {
if (this.post?.isDestroyed || this.post?.isDestroying) { 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); this.set('post.lexical', this.post.lexicalScratch || null);
// Set a default title // Set a default title
if (!this.get('post.titleScratch').trim()) { if (!this.post.titleScratch?.trim()) {
this.set('post.titleScratch', DEFAULT_TITLE); this.set('post.titleScratch', DEFAULT_TITLE);
} }
@ -1042,6 +1054,8 @@ export default class LexicalEditorController extends Controller {
// reset everything ready for a new post // reset everything ready for a new post
this.reset(); this.reset();
this._setPostState = post.currentState.stateName;
this.set('post', post); this.set('post', post);
this.backgroundLoaderTask.perform(); this.backgroundLoaderTask.perform();
@ -1211,6 +1225,8 @@ export default class LexicalEditorController extends Controller {
this._leaveConfirmed = false; this._leaveConfirmed = false;
this._saveOnLeavePerformed = false; this._saveOnLeavePerformed = false;
this._setPostState = null;
this.set('post', null); this.set('post', null);
this.set('hasDirtyAttributes', false); this.set('hasDirtyAttributes', false);
this.set('shouldFocusTitle', false); this.set('shouldFocusTitle', false);

View file

@ -1,5 +1,6 @@
import EmberObject from '@ember/object'; import EmberObject from '@ember/object';
import RSVP from 'rsvp'; import RSVP from 'rsvp';
import {authenticateSession} from 'ember-simple-auth/test-support';
import {defineProperty} from '@ember/object'; import {defineProperty} from '@ember/object';
import {describe, it} from 'mocha'; import {describe, it} from 'mocha';
import {expect} from 'chai'; import {expect} from 'chai';
@ -424,4 +425,53 @@ describe('Unit: Controller: lexical-editor', function () {
expect(isDirty).to.be.false; 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');
});
});
}); });