From 19db3e9be56bad8d577fda1d5a4b844e5e60e90b Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 9 Oct 2026 13:38:27 +0100 Subject: [PATCH] Fixed Date values being cloned to {} in prepareStackForUser prepareStackForUser deep clones the error before Ghost renders it, and the inline deepCloneValue that replaced @stdlib/utils-copy in a447d779 copied every non-array object as a plain object. Date has no own enumerable keys, so any Date in errorDetails or context came out as {}. UpdateCollisionError from @tryghost/bookshelf-collision carries clientUpdatedAt and serverUpdatedAt as Dates, so every 409 response from Ghost's API has returned both as {}. Admin reports these collisions to Sentry to tell a concurrent writer apart from a retried save, which needs the timestamps. Dates are now cloned as Dates, so they serialize to ISO strings again. --- packages/errors/src/utils.ts | 3 +++ packages/errors/test/utils.test.ts | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts index 6f49d4052..f513bcac7 100644 --- a/packages/errors/src/utils.ts +++ b/packages/errors/src/utils.ts @@ -21,6 +21,9 @@ function deepCloneValue(value: any): any { if (Array.isArray(value)) { return value.map(deepCloneValue); } + if (value instanceof Date) { + return new Date(value.getTime()); + } const clone: AnyObject = {}; for (const key of Object.keys(value)) { clone[key] = deepCloneValue(value[key]); diff --git a/packages/errors/test/utils.test.ts b/packages/errors/test/utils.test.ts index 83f181723..53f038ad1 100644 --- a/packages/errors/test/utils.test.ts +++ b/packages/errors/test/utils.test.ts @@ -54,6 +54,26 @@ describe('Error Utils', function () { assert.equal(processedError.errorDetails[0].id, 1); }); + it('preserves Date values in errorDetails', function () { + const serverUpdatedAt = new Date('2026-10-09T10:00:00.000Z'); + const ghostError = new errors.UpdateCollisionError({ + message: 'Saving failed! Someone else is editing this post.', + errorDetails: { + serverUpdatedAt, + }, + }); + + const processedError = utils.prepareStackForUser(ghostError); + const clonedDate = processedError.errorDetails.serverUpdatedAt; + + assert.ok(clonedDate instanceof Date); + assert.notEqual(clonedDate, serverUpdatedAt); + assert.equal(clonedDate.getTime(), serverUpdatedAt.getTime()); + assert.deepEqual(JSON.parse(JSON.stringify(processedError.errorDetails)), { + serverUpdatedAt: '2026-10-09T10:00:00.000Z', + }); + }); + it('Preserves the stack trace', function () { const errorCreatingFunction = () => { return new Error('Original error');