Merge pull request #32710 from overleaf/mg-project-history-metrics

Add diagnostic annotations to LazyStringFileData toEager errors

GitOrigin-RevId: 47575586bb869d65e4eb443cc9f1215b6f245255
This commit is contained in:
Malik Glossop
2026-04-28 13:35:03 +02:00
committed by Copybot
parent 6296e7911c
commit ba182f8275
3 changed files with 138 additions and 4 deletions

View File

@@ -4,11 +4,13 @@
const _ = require('lodash')
const assert = require('check-types').assert
const OError = require('@overleaf/o-error')
const Blob = require('../blob')
const FileData = require('./')
const EagerStringFileData = require('./string_file_data')
const EditOperation = require('../operation/edit_operation')
const EditOperationBuilder = require('../operation/edit_operation_builder')
const TextOperation = require('../operation/text_operation')
/**
* @import { BlobStore, ReadonlyBlobStore, RangesBlob, RawHashFileData, RawLazyStringFileData } from '../types'
@@ -152,7 +154,25 @@ class LazyStringFileData extends FileData {
ranges?.comments,
ranges?.trackedChanges
)
applyOperations(this.operations, file)
try {
applyOperations(this.operations, file)
} catch (err) {
const firstOp = this.operations[0]
const firstOpBaseLength =
firstOp instanceof TextOperation ? firstOp.baseLength : undefined
throw OError.tag(err, 'failed to apply operations in toEager', {
blobHash: this.hash,
blobContentLength: content.length,
metadataStringLength: this.stringLength,
totalOperations: this.operations.length,
firstOpBaseLength,
contentMatchesMetadata: content.length === this.stringLength,
contentMatchesFirstOp:
typeof firstOpBaseLength === 'number'
? content.length === firstOpBaseLength
: undefined,
})
}
return file
}
@@ -172,7 +192,18 @@ class LazyStringFileData extends FileData {
* @param {EditOperation} operation
*/
edit(operation) {
this.stringLength = operation.applyToLength(this.stringLength)
try {
this.stringLength = operation.applyToLength(this.stringLength)
} catch (err) {
const baseLength =
operation instanceof TextOperation ? operation.baseLength : undefined
throw OError.tag(err, 'failed to apply operation length in edit', {
blobHash: this.hash,
metadataStringLength: this.stringLength,
operationBaseLength: baseLength,
totalExistingOperations: this.operations.length,
})
}
this.operations.push(operation)
}
@@ -205,7 +236,17 @@ class LazyStringFileData extends FileData {
* @returns {void}
*/
function applyOperations(operations, file) {
_.each(operations, operation => operation.apply(file))
for (let i = 0; i < operations.length; i++) {
try {
operations[i].apply(file)
} catch (err) {
throw OError.tag(err, 'operation failed during applyOperations', {
operationIndex: i,
totalOperations: operations.length,
currentContentLength: file.getStringLength(),
})
}
}
}
module.exports = LazyStringFileData

View File

@@ -4,6 +4,7 @@
const _ = require('lodash')
const { expect } = require('chai')
const sinon = require('sinon')
const OError = require('@overleaf/o-error')
const ot = require('../..')
const File = ot.File
@@ -202,4 +203,85 @@ describe('LazyStringFileData', function () {
expect(fileData.hash).to.equal(stored.hash)
expect(fileData.operations).to.deep.equal([])
})
describe('error annotation', function () {
it('annotates errors in toEager with blob and operation metadata', async function () {
const testHash = this.fileHash
// Create a file with content length 19 ('the quick brown fox')
// then queue an operation that expects a different base length
const fileData = new LazyStringFileData(testHash, undefined, 19)
// This operation expects base length 999, which won't match the blob
const badOp = new TextOperation()
badOp.retain(999)
fileData.operations.push(badOp)
// Manually set stringLength to match the op's baseLength: pushing directly
// to operations bypasses edit(), which is what normally updates stringLength
fileData.stringLength = 999
try {
await fileData.toEager(this.blobStore)
expect.fail('should have thrown')
} catch (err) {
const info = OError.getFullInfo(err)
expect(info).to.have.property('blobHash', testHash)
expect(info).to.have.property('blobContentLength', 19)
expect(info).to.have.property('metadataStringLength', 999)
expect(info).to.have.property('totalOperations', 1)
expect(info).to.have.property('operationIndex', 0)
expect(info).to.have.property('currentContentLength', 19)
expect(info).to.have.property('firstOpBaseLength', 999)
expect(info).to.have.property('contentMatchesMetadata', false)
expect(info).to.have.property('contentMatchesFirstOp', false)
}
})
it('annotates errors in edit with operation and metadata context', function () {
const testHash = this.fileHash
const fileData = new LazyStringFileData(testHash, undefined, 19)
// Queue one valid operation first so totalExistingOperations > 0
fileData.edit(new TextOperation().retain(19).insert('!'))
// This operation expects base length 999, mismatching stringLength of 20
const badOp = new TextOperation()
badOp.retain(999)
try {
fileData.edit(badOp)
expect.fail('should have thrown')
} catch (err) {
const info = OError.getFullInfo(err)
expect(info).to.have.property('blobHash', testHash)
expect(info).to.have.property('metadataStringLength', 20)
expect(info).to.have.property('operationBaseLength', 999)
expect(info).to.have.property('totalExistingOperations', 1)
}
})
it('annotates errors in applyOperations with the failing operation index', async function () {
const testHash = this.fileHash
// Content is 'the quick brown fox' (length 19)
const fileData = new LazyStringFileData(testHash, undefined, 19)
// First op is valid: insert at end
const goodOp = new TextOperation().retain(19).insert('!')
// Second op expects length 999 — will fail
const badOp = new TextOperation()
badOp.retain(999)
fileData.operations.push(goodOp)
fileData.operations.push(badOp)
fileData.stringLength = 999
try {
await fileData.toEager(this.blobStore)
expect.fail('should have thrown')
} catch (err) {
const info = OError.getFullInfo(err)
// The second operation (index 1) should be the one that fails
expect(info).to.have.property('operationIndex', 1)
expect(info).to.have.property('totalOperations', 2)
expect(info).to.have.property('currentContentLength', 20)
// toEager also tags with blob metadata
expect(info).to.have.property('blobHash', testHash)
expect(info).to.have.property('blobContentLength', 19)
expect(info).to.have.property('metadataStringLength', 999)
}
})
})
})