Skip to content

Commit

Permalink
Added sentry logging to local revisions service for localStorage erro…
Browse files Browse the repository at this point in the history
…rs (#21078)

no issue

- Added Sentry logs to capture how often we are running into
`QuotaExceededErrors` when saving local revisions to localStorage, to
help in deciding if localStorage is sufficient, or if we need to expand
to e.g. IndexedDB.
- Also adds some handling to ignore errors when calling
`localStorage.setItem()` elsewhere in the admin app to avoid crashing if
localStorage isn't supported or the quota is exceeded.
  • Loading branch information
cmraible authored Sep 25, 2024
1 parent c121149 commit 0125f52
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 74 deletions.
24 changes: 16 additions & 8 deletions ghost/admin/app/components/editor/modals/publish-flow/confirm.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,28 @@ export default class PublishFlowOptions extends Component {
yield this.args.saveTask.perform();

if (this.args.publishOptions.isScheduled) {
localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
try {
localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
} catch (e) {
// ignore localStorage errors
}
if (this.args.publishOptions.post.displayName !== 'page') {
this.router.transitionTo('posts');
} else {
this.router.transitionTo('pages');
}
} else {
localStorage.setItem('ghost-last-published-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
try {
localStorage.setItem('ghost-last-published-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
} catch (e) {
// ignore localStorage errors
}
if (this.args.publishOptions.post.displayName !== 'page') {
if (this.args.publishOptions.post.hasEmail) {
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
Expand Down
19 changes: 6 additions & 13 deletions ghost/admin/app/controllers/lexical-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,10 @@ export default class LexicalEditorController extends Controller {
updateScratch(lexical) {
const lexicalString = JSON.stringify(lexical);
this.set('post.lexicalScratch', lexicalString);

try {
// schedule a local revision save
if (this.post.status === 'draft') {
this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), lexical: lexicalString});
}
} catch (err) {
// ignore errors
this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), lexical: lexicalString});
} catch (e) {
// ignore revision save errors
}

// save 3 seconds after last edit
Expand All @@ -341,12 +337,9 @@ export default class LexicalEditorController extends Controller {
updateTitleScratch(title) {
this.set('post.titleScratch', title);
try {
// schedule a local revision save
if (this.post.status === 'draft') {
this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), title: title});
}
} catch (err) {
// ignore errors
this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), title: title});
} catch (e) {
// ignore revision save errors
}
}

Expand Down
48 changes: 31 additions & 17 deletions ghost/admin/app/services/local-revisions.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/ember';
import Service, {inject as service} from '@ember/service';
import config from 'ghost-admin/config/environment';
import {task, timeout} from 'ember-concurrency';
Expand All @@ -13,6 +14,7 @@ export default class LocalRevisionsService extends Service {
}
this.MIN_REVISION_TIME = this.isTesting ? 50 : 60000; // 1 minute in ms
this.performSave = this.performSave.bind(this);
this.storage = window.localStorage;
}

@service store;
Expand Down Expand Up @@ -42,15 +44,19 @@ export default class LocalRevisionsService extends Service {
*/
@task({keepLatest: true})
*saveTask(type, data) {
const currentTime = Date.now();
if (!this.lastRevisionTime || currentTime - this.lastRevisionTime > this.MIN_REVISION_TIME) {
yield this.performSave(type, data);
this.lastRevisionTime = currentTime;
} else {
const waitTime = this.MIN_REVISION_TIME - (currentTime - this.lastRevisionTime);
yield timeout(waitTime);
yield this.performSave(type, data);
this.lastRevisionTime = Date.now();
try {
const currentTime = Date.now();
if (!this.lastRevisionTime || currentTime - this.lastRevisionTime > this.MIN_REVISION_TIME) {
yield this.performSave(type, data);
this.lastRevisionTime = currentTime;
} else {
const waitTime = this.MIN_REVISION_TIME - (currentTime - this.lastRevisionTime);
yield timeout(waitTime);
yield this.performSave(type, data);
this.lastRevisionTime = Date.now();
}
} catch (err) {
Sentry.captureException(err, {tags: {localRevisions: 'saveTaskError'}});
}
}

Expand All @@ -70,8 +76,8 @@ export default class LocalRevisionsService extends Service {
try {
const allKeys = this.keys();
allKeys.push(key);
localStorage.setItem(this._indexKey, JSON.stringify(allKeys));
localStorage.setItem(key, JSON.stringify(data));
this.storage.setItem(this._indexKey, JSON.stringify(allKeys));
this.storage.setItem(key, JSON.stringify(data));

// Apply the filter after saving
this.filterRevisions(data.id);
Expand All @@ -84,11 +90,17 @@ export default class LocalRevisionsService extends Service {

// If there are any revisions, remove the oldest one and try to save again
if (this.keys().length) {
Sentry.captureMessage('LocalStorage quota exceeded. Removing old revisions.', {tags: {localRevisions: 'quotaExceeded'}});
this.removeOldest();
return this.performSave(type, data);
}
// LocalStorage is full and there are no revisions to remove
// We can't save the revision
Sentry.captureMessage('LocalStorage quota exceeded. Unable to save revision.', {tags: {localRevisions: 'quotaExceededNoSpace'}});
return;
} else {
Sentry.captureException(err, {tags: {localRevisions: 'saveError'}});
return;
}
}
}
Expand All @@ -99,7 +111,9 @@ export default class LocalRevisionsService extends Service {
* @param {object} data - serialized post data
*/
scheduleSave(type, data) {
this.saveTask.perform(type, data);
if (data && data.status && data.status === 'draft') {
this.saveTask.perform(type, data);
}
}

/**
Expand All @@ -108,7 +122,7 @@ export default class LocalRevisionsService extends Service {
* @returns {string | null}
*/
find(key) {
return JSON.parse(localStorage.getItem(key));
return JSON.parse(this.storage.getItem(key));
}

/**
Expand All @@ -119,7 +133,7 @@ export default class LocalRevisionsService extends Service {
findAll(prefix = this._prefix) {
const keys = this.keys(prefix);
const revisions = keys.map((key) => {
const revision = JSON.parse(localStorage.getItem(key));
const revision = JSON.parse(this.storage.getItem(key));
return {
key,
...revision
Expand All @@ -137,13 +151,13 @@ export default class LocalRevisionsService extends Service {
* @param {string} key
*/
remove(key) {
localStorage.removeItem(key);
this.storage.removeItem(key);
const keys = this.keys();
let index = keys.indexOf(key);
if (index !== -1) {
keys.splice(index, 1);
}
localStorage.setItem(this._indexKey, JSON.stringify(keys));
this.storage.setItem(this._indexKey, JSON.stringify(keys));
}

/**
Expand Down Expand Up @@ -172,7 +186,7 @@ export default class LocalRevisionsService extends Service {
* @returns {string[]}
*/
keys(prefix = undefined) {
let keys = JSON.parse(localStorage.getItem(this._indexKey) || '[]');
let keys = JSON.parse(this.storage.getItem(this._indexKey) || '[]');
if (prefix) {
keys = keys.filter(key => key.startsWith(prefix));
}
Expand Down
Loading

0 comments on commit 0125f52

Please sign in to comment.