mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-10 06:39:01 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -110,7 +110,7 @@ function sentryReporter() {
|
||||
/extensions\//i,
|
||||
/^chrome:\/\//i,
|
||||
],
|
||||
beforeSend(event) {
|
||||
beforeSend(event, hint) {
|
||||
// Limit number of events sent to Sentry to 100 events "per page load",
|
||||
// (i.e. the cap will be reset if the page is reloaded). This prevent
|
||||
// hitting their server-side event cap.
|
||||
@@ -144,6 +144,17 @@ function sentryReporter() {
|
||||
return null
|
||||
}
|
||||
|
||||
// Extract OError tag info so it appears in Sentry extra for all
|
||||
// captured errors, including auto-captured unhandled rejections
|
||||
// that bypass our captureException() wrapper.
|
||||
const originalException = hint?.originalException
|
||||
if (originalException && typeof originalException === 'object') {
|
||||
const oErrorInfo = OError.getFullInfo(originalException)
|
||||
if (Object.keys(oErrorInfo).length > 0) {
|
||||
event.extra = { ...event.extra, ...oErrorInfo }
|
||||
}
|
||||
}
|
||||
|
||||
return sanitizeUrls(event)
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user