@ckeditor/ckeditor5-watchdog 46.0.2 → 46.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +645 -18
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/actionsrecorder.d.ts +259 -0
- package/src/actionsrecorder.js +627 -0
- package/src/actionsrecorderconfig.d.ts +204 -0
- package/src/actionsrecorderconfig.js +5 -0
- package/src/augmentation.d.ts +5 -0
- package/src/editorwatchdog.js +21 -14
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -0
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
import { throttle, isElement, cloneDeepWith } from 'es-toolkit/compat';
|
|
5
|
+
import { throttle, isElement, cloneDeepWith, isPlainObject } from 'es-toolkit/compat';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
@@ -672,27 +672,37 @@ function isObject(structure) {
|
|
|
672
672
|
// `as any` to avoid linking from external private repo.
|
|
673
673
|
const parsedCommentThreads = JSON.parse(this._data.commentThreads);
|
|
674
674
|
const parsedSuggestions = JSON.parse(this._data.suggestions);
|
|
675
|
-
|
|
676
|
-
const channelId = this.editor.config.get('collaboration.channelId');
|
|
675
|
+
if (this.editor.plugins.has('CommentsRepository')) {
|
|
677
676
|
const commentsRepository = this.editor.plugins.get('CommentsRepository');
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
677
|
+
// First, remove the existing comments that were created by integration plugins during initialization.
|
|
678
|
+
// These comments may be outdated, and new instances will be created in the next step based on the saved data.
|
|
679
|
+
for (const commentThread of commentsRepository.getCommentThreads()){
|
|
680
|
+
// Use the internal API since it removes the comment thread directly and does not trigger events
|
|
681
|
+
// that could cause side effects, such as removing markers.
|
|
682
|
+
commentsRepository._removeCommentThread({
|
|
683
|
+
threadId: commentThread.id
|
|
684
|
+
});
|
|
681
685
|
}
|
|
682
|
-
|
|
683
|
-
channelId
|
|
684
|
-
|
|
686
|
+
parsedCommentThreads.forEach((commentThreadData)=>{
|
|
687
|
+
const channelId = this.editor.config.get('collaboration.channelId');
|
|
688
|
+
const commentsRepository = this.editor.plugins.get('CommentsRepository');
|
|
689
|
+
commentsRepository.addCommentThread({
|
|
690
|
+
channelId,
|
|
691
|
+
...commentThreadData
|
|
692
|
+
});
|
|
685
693
|
});
|
|
686
|
-
}
|
|
687
|
-
|
|
694
|
+
}
|
|
695
|
+
if (this.editor.plugins.has('TrackChangesEditing')) {
|
|
688
696
|
const trackChangesEditing = this.editor.plugins.get('TrackChangesEditing');
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
trackChangesEditing.addSuggestionData(suggestionData);
|
|
697
|
+
// First, remove the existing suggestions that were created by integration plugins during initialization.
|
|
698
|
+
// These suggestions may be outdated, and new instances will be created in the next step based on the saved data.
|
|
699
|
+
for (const suggestion of trackChangesEditing.getSuggestions()){
|
|
700
|
+
trackChangesEditing._removeSuggestion(suggestion);
|
|
694
701
|
}
|
|
695
|
-
|
|
702
|
+
parsedSuggestions.forEach((suggestionData)=>{
|
|
703
|
+
trackChangesEditing.addSuggestionData(suggestionData);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
696
706
|
}
|
|
697
707
|
}
|
|
698
708
|
|
|
@@ -1080,5 +1090,622 @@ const mainQueueId = Symbol('MainQueueId');
|
|
|
1080
1090
|
];
|
|
1081
1091
|
}
|
|
1082
1092
|
|
|
1083
|
-
|
|
1093
|
+
/**
|
|
1094
|
+
* A plugin that records user actions and editor state changes for debugging purposes. It tracks commands execution, model operations,
|
|
1095
|
+
* UI interactions, and document events. It just collects data locally, and does not send it anywhere, integrator is responsible
|
|
1096
|
+
* for gathering data from this plugin for further processing.
|
|
1097
|
+
*
|
|
1098
|
+
* **Important! `ActionsRecorder` is an experimental feature, and may become deprecated.**
|
|
1099
|
+
*
|
|
1100
|
+
* By default, plugin stores latest 1000 action entries. Integrator can register an `onError` callback to collect those entries
|
|
1101
|
+
* in case of exception. Integrator should augment this data with application specific data such as page-id or session-id,
|
|
1102
|
+
* depending on the application. Augmented data should be processed by the integrator, for example integrator should send it
|
|
1103
|
+
* to some data collecting endpoint for later analysis.
|
|
1104
|
+
*
|
|
1105
|
+
* Example:
|
|
1106
|
+
*
|
|
1107
|
+
* ```ts
|
|
1108
|
+
* ClassicEditor
|
|
1109
|
+
* .create( editorElement, {
|
|
1110
|
+
* plugins: [ ActionsRecorder, ... ],
|
|
1111
|
+
* actionsRecorder: {
|
|
1112
|
+
* maxEntries: 1000, // This is the default value and could be adjusted.
|
|
1113
|
+
*
|
|
1114
|
+
* onError( error, entries ) {
|
|
1115
|
+
* console.error( 'ActionsRecorder - Error detected:', error );
|
|
1116
|
+
* console.warn( 'Actions recorded before error:', entries );
|
|
1117
|
+
*
|
|
1118
|
+
* this.flushEntries();
|
|
1119
|
+
*
|
|
1120
|
+
* // Integrator should send and store the entries. The error is already in the last entry in serializable form.
|
|
1121
|
+
* }
|
|
1122
|
+
* }
|
|
1123
|
+
* } )
|
|
1124
|
+
* .then( ... )
|
|
1125
|
+
* .catch( ... );
|
|
1126
|
+
* ```
|
|
1127
|
+
*
|
|
1128
|
+
* Alternatively integrator could continuously collect actions in batches and send them to theirs endpoint for later analysis:
|
|
1129
|
+
*
|
|
1130
|
+
* ```ts
|
|
1131
|
+
* ClassicEditor
|
|
1132
|
+
* .create( editorElement, {
|
|
1133
|
+
* plugins: [ ActionsRecorder, ... ],
|
|
1134
|
+
* actionsRecorder: {
|
|
1135
|
+
* maxEntries: 50, // This is the batch size.
|
|
1136
|
+
*
|
|
1137
|
+
* onMaxEntries() {
|
|
1138
|
+
* const entries = this.getEntries();
|
|
1139
|
+
*
|
|
1140
|
+
* this.flushEntries();
|
|
1141
|
+
*
|
|
1142
|
+
* console.log( 'ActionsRecorder - Batch of entries:', entries );
|
|
1143
|
+
*
|
|
1144
|
+
* // Integrator should send and store the entries.
|
|
1145
|
+
* },
|
|
1146
|
+
*
|
|
1147
|
+
* onError( error, entries ) {
|
|
1148
|
+
* console.error( 'ActionsRecorder - Error detected:', error );
|
|
1149
|
+
* console.warn( 'Actions recorded before error:', entries );
|
|
1150
|
+
*
|
|
1151
|
+
* this.flushEntries();
|
|
1152
|
+
*
|
|
1153
|
+
* // Integrator should send and store the entries. The error is already in the last entry in serializable form.
|
|
1154
|
+
* }
|
|
1155
|
+
* }
|
|
1156
|
+
* } )
|
|
1157
|
+
* .then( ... )
|
|
1158
|
+
* .catch( ... );
|
|
1159
|
+
* ```
|
|
1160
|
+
*
|
|
1161
|
+
* See {@link module:watchdog/actionsrecorderconfig~ActionsRecorderConfig plugin configuration} for more details.
|
|
1162
|
+
*
|
|
1163
|
+
*/ class ActionsRecorder {
|
|
1164
|
+
/**
|
|
1165
|
+
* The editor instance.
|
|
1166
|
+
*/ editor;
|
|
1167
|
+
/**
|
|
1168
|
+
* Array storing all recorded action entries with their context and state snapshots.
|
|
1169
|
+
*/ _entries = [];
|
|
1170
|
+
/**
|
|
1171
|
+
* Stack tracking nested action frames to maintain call hierarchy.
|
|
1172
|
+
*/ _frameStack = [];
|
|
1173
|
+
/**
|
|
1174
|
+
* Set of already reported errors used to notify only once for each error (not on every try-catch nested block).
|
|
1175
|
+
*/ _errors = new Set();
|
|
1176
|
+
/**
|
|
1177
|
+
* Maximum number of action entries to keep in memory.
|
|
1178
|
+
*/ _maxEntries;
|
|
1179
|
+
/**
|
|
1180
|
+
* Error callback.
|
|
1181
|
+
*/ _errorCallback;
|
|
1182
|
+
/**
|
|
1183
|
+
* Filter function to determine which entries should be stored.
|
|
1184
|
+
*/ _filterCallback;
|
|
1185
|
+
/**
|
|
1186
|
+
* Callback triggered every time count of recorded entries reaches maxEntries.
|
|
1187
|
+
*/ _maxEntriesCallback;
|
|
1188
|
+
/**
|
|
1189
|
+
* @inheritDoc
|
|
1190
|
+
*/ static get pluginName() {
|
|
1191
|
+
return 'ActionsRecorder';
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* @inheritDoc
|
|
1195
|
+
*/ static get isOfficialPlugin() {
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* @inheritDoc
|
|
1200
|
+
*/ constructor(editor){
|
|
1201
|
+
this.editor = editor;
|
|
1202
|
+
editor.config.define('actionsRecorder.maxEntries', 1000);
|
|
1203
|
+
const config = editor.config.get('actionsRecorder');
|
|
1204
|
+
this._maxEntries = config.maxEntries;
|
|
1205
|
+
this._filterCallback = config.onFilter;
|
|
1206
|
+
this._errorCallback = config.onError;
|
|
1207
|
+
this._maxEntriesCallback = config.onMaxEntries || this._maxEntriesDefaultHandler;
|
|
1208
|
+
this._tapCommands();
|
|
1209
|
+
this._tapOperationApply();
|
|
1210
|
+
this._tapModelMethods();
|
|
1211
|
+
this._tapModelSelection();
|
|
1212
|
+
this._tapComponentFactory();
|
|
1213
|
+
this._tapViewDocumentEvents();
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Returns all recorded action entries.
|
|
1217
|
+
*/ getEntries() {
|
|
1218
|
+
// Return a shallow copy instead of reference as this array could be modified.
|
|
1219
|
+
return this._entries.slice();
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Flushes all recorded entries.
|
|
1223
|
+
*/ flushEntries() {
|
|
1224
|
+
this._entries = [];
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Creates a new action frame and adds it to the recording stack.
|
|
1228
|
+
*
|
|
1229
|
+
* @param action The name/type of the action being recorded.
|
|
1230
|
+
* @param params Optional parameters associated with the event.
|
|
1231
|
+
* @returns The created call frame object.
|
|
1232
|
+
*/ _enterFrame(action, params) {
|
|
1233
|
+
const callFrame = {
|
|
1234
|
+
timeStamp: new Date().toISOString(),
|
|
1235
|
+
...this._frameStack.length && {
|
|
1236
|
+
parentEntry: this._frameStack.at(-1)
|
|
1237
|
+
},
|
|
1238
|
+
action,
|
|
1239
|
+
params: params?.map((param)=>serializeValue(param)),
|
|
1240
|
+
before: this._buildStateSnapshot()
|
|
1241
|
+
};
|
|
1242
|
+
// Apply filter if configured, only add to entries if filter passes.
|
|
1243
|
+
if (!this._filterCallback || this._filterCallback(callFrame, this._entries)) {
|
|
1244
|
+
// Add the call frame to the entries.
|
|
1245
|
+
this._entries.push(callFrame);
|
|
1246
|
+
}
|
|
1247
|
+
this._frameStack.push(callFrame);
|
|
1248
|
+
return callFrame;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Closes an action frame and records its final state and results.
|
|
1252
|
+
*
|
|
1253
|
+
* @param callFrame The call frame to close.
|
|
1254
|
+
* @param result Optional result value from the action.
|
|
1255
|
+
* @param error Optional error that occurred during the action.
|
|
1256
|
+
*/ _leaveFrame(callFrame, result, error) {
|
|
1257
|
+
const topFrame = this._frameStack.pop();
|
|
1258
|
+
// Handle scenario when the stack has been cleared in the meantime
|
|
1259
|
+
// or the callFrame is not the top frame.
|
|
1260
|
+
if (!topFrame || topFrame !== callFrame) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (result !== undefined) {
|
|
1264
|
+
topFrame.result = serializeValue(result);
|
|
1265
|
+
}
|
|
1266
|
+
if (error) {
|
|
1267
|
+
topFrame.error = serializeValue(error);
|
|
1268
|
+
}
|
|
1269
|
+
topFrame.after = this._buildStateSnapshot();
|
|
1270
|
+
if (error) {
|
|
1271
|
+
this._callErrorCallback(error);
|
|
1272
|
+
}
|
|
1273
|
+
if (this._frameStack.length == 0) {
|
|
1274
|
+
this._errors.clear();
|
|
1275
|
+
}
|
|
1276
|
+
// Enforce max entries limit after leaving the frame so that complete entry is provided.
|
|
1277
|
+
if (this._entries.length >= this._maxEntries) {
|
|
1278
|
+
this._maxEntriesCallback();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Builds a snapshot of the current editor state including document version,
|
|
1283
|
+
* read-only status, focus state, and model selection.
|
|
1284
|
+
*
|
|
1285
|
+
* @returns An object containing the current editor state snapshot.
|
|
1286
|
+
*/ _buildStateSnapshot() {
|
|
1287
|
+
const { model, isReadOnly, editing } = this.editor;
|
|
1288
|
+
return {
|
|
1289
|
+
documentVersion: model.document.version,
|
|
1290
|
+
editorReadOnly: isReadOnly,
|
|
1291
|
+
editorFocused: editing.view.document.isFocused,
|
|
1292
|
+
modelSelection: serializeValue(model.document.selection)
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Sets up recording for all editor commands, both existing and future ones.
|
|
1297
|
+
* Taps into the command execution to track when commands are run.
|
|
1298
|
+
*/ _tapCommands() {
|
|
1299
|
+
// Tap already registered commands.
|
|
1300
|
+
for (const [commandName, command] of this.editor.commands){
|
|
1301
|
+
this._tapCommand(commandName, command);
|
|
1302
|
+
}
|
|
1303
|
+
// Tap commands registered after the constructor was called.
|
|
1304
|
+
tapObjectMethod(this.editor.commands, 'add', {
|
|
1305
|
+
before: (callContext, [commandName, command])=>{
|
|
1306
|
+
this._tapCommand(commandName, command);
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Sets up recording for model operation applications.
|
|
1313
|
+
* Tracks when operations are applied to the model document.
|
|
1314
|
+
*/ _tapOperationApply() {
|
|
1315
|
+
tapObjectMethod(this.editor.model, 'applyOperation', {
|
|
1316
|
+
before: (callContext, [operation])=>{
|
|
1317
|
+
// Ignore operations applied to document fragments.
|
|
1318
|
+
if (operation.baseVersion === null) {
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
callContext.callFrame = this._enterFrame('model.applyOperation', [
|
|
1322
|
+
operation
|
|
1323
|
+
]);
|
|
1324
|
+
return true;
|
|
1325
|
+
},
|
|
1326
|
+
after: (callContext)=>{
|
|
1327
|
+
this._leaveFrame(callContext.callFrame);
|
|
1328
|
+
},
|
|
1329
|
+
error: (callContext, error)=>{
|
|
1330
|
+
this._leaveFrame(callContext.callFrame, undefined, error);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Sets up recording for key model methods like insertContent, insertObject, and deleteContent.
|
|
1336
|
+
* These methods represent high-level model manipulation operations.
|
|
1337
|
+
*/ _tapModelMethods() {
|
|
1338
|
+
for (const methodName of [
|
|
1339
|
+
'insertContent',
|
|
1340
|
+
'insertObject',
|
|
1341
|
+
'deleteContent'
|
|
1342
|
+
]){
|
|
1343
|
+
tapObjectMethod(this.editor.model, methodName, {
|
|
1344
|
+
before: (callContext, ...params)=>{
|
|
1345
|
+
callContext.callFrame = this._enterFrame(`model.${methodName}`, params);
|
|
1346
|
+
return true;
|
|
1347
|
+
},
|
|
1348
|
+
after: (callContext, result)=>{
|
|
1349
|
+
this._leaveFrame(callContext.callFrame, result);
|
|
1350
|
+
},
|
|
1351
|
+
error: (callContext, error)=>{
|
|
1352
|
+
this._leaveFrame(callContext.callFrame, undefined, error);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Sets up recording for model selection changes.
|
|
1359
|
+
* Tracks when the selection range, attributes, or markers change.
|
|
1360
|
+
*/ _tapModelSelection() {
|
|
1361
|
+
const events = [
|
|
1362
|
+
'change:range',
|
|
1363
|
+
'change:attribute',
|
|
1364
|
+
'change:marker'
|
|
1365
|
+
];
|
|
1366
|
+
this._tapFireMethod(this.editor.model.document.selection, events, {
|
|
1367
|
+
eventSource: 'model-selection'
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Sets up recording for a specific command execution.
|
|
1372
|
+
*
|
|
1373
|
+
* @param commandName The name of the command to record.
|
|
1374
|
+
* @param command The command instance to tap into.
|
|
1375
|
+
*/ _tapCommand(commandName, command) {
|
|
1376
|
+
tapObjectMethod(command, 'execute', {
|
|
1377
|
+
before: (callContext, params)=>{
|
|
1378
|
+
callContext.callFrame = this._enterFrame(`commands.${commandName}:execute`, params);
|
|
1379
|
+
return true;
|
|
1380
|
+
},
|
|
1381
|
+
after: (callContext, result)=>{
|
|
1382
|
+
this._leaveFrame(callContext.callFrame, result);
|
|
1383
|
+
},
|
|
1384
|
+
error: (callContext, error)=>{
|
|
1385
|
+
this._leaveFrame(callContext.callFrame, undefined, error);
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Sets up recording for UI component factory creation and component interactions.
|
|
1391
|
+
* Tracks when components are created and their execute events.
|
|
1392
|
+
*/ _tapComponentFactory() {
|
|
1393
|
+
tapObjectMethod(this.editor.ui.componentFactory, 'create', {
|
|
1394
|
+
before: (callContext, [componentName])=>{
|
|
1395
|
+
callContext.componentName = componentName;
|
|
1396
|
+
callContext.callFrame = this._enterFrame(`component-factory.create:${componentName}`);
|
|
1397
|
+
return true;
|
|
1398
|
+
},
|
|
1399
|
+
after: (callContext, componentInstance)=>{
|
|
1400
|
+
const executeContext = {
|
|
1401
|
+
...callContext,
|
|
1402
|
+
eventSource: `component.${callContext.componentName}`
|
|
1403
|
+
};
|
|
1404
|
+
if (typeof componentInstance.fire == 'function') {
|
|
1405
|
+
this._tapFireMethod(componentInstance, [
|
|
1406
|
+
'execute'
|
|
1407
|
+
], executeContext);
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof componentInstance.panelView?.fire == 'function') {
|
|
1410
|
+
this._tapFireMethod(componentInstance.panelView, [
|
|
1411
|
+
'execute'
|
|
1412
|
+
], executeContext);
|
|
1413
|
+
}
|
|
1414
|
+
if (typeof componentInstance.buttonView?.actionView?.fire == 'function') {
|
|
1415
|
+
this._tapFireMethod(componentInstance.buttonView.actionView, [
|
|
1416
|
+
'execute'
|
|
1417
|
+
], executeContext);
|
|
1418
|
+
}
|
|
1419
|
+
this._leaveFrame(callContext.callFrame);
|
|
1420
|
+
},
|
|
1421
|
+
error: (callContext, error)=>{
|
|
1422
|
+
this._leaveFrame(callContext.callFrame, undefined, error);
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Sets up recording for view document events like clicks, keyboard input,
|
|
1428
|
+
* selection changes, and other user interactions.
|
|
1429
|
+
*/ _tapViewDocumentEvents() {
|
|
1430
|
+
const events = [
|
|
1431
|
+
'click',
|
|
1432
|
+
'mousedown',
|
|
1433
|
+
'mouseup',
|
|
1434
|
+
'pointerdown',
|
|
1435
|
+
'pointerup',
|
|
1436
|
+
'focus',
|
|
1437
|
+
'blur',
|
|
1438
|
+
'keydown',
|
|
1439
|
+
'keyup',
|
|
1440
|
+
'selectionChange',
|
|
1441
|
+
'compositionstart',
|
|
1442
|
+
'compositionend',
|
|
1443
|
+
'beforeinput',
|
|
1444
|
+
'mutations',
|
|
1445
|
+
'enter',
|
|
1446
|
+
'delete',
|
|
1447
|
+
'insertText',
|
|
1448
|
+
'paste',
|
|
1449
|
+
'copy',
|
|
1450
|
+
'cut',
|
|
1451
|
+
'dragstart',
|
|
1452
|
+
'drop'
|
|
1453
|
+
];
|
|
1454
|
+
this._tapFireMethod(this.editor.editing.view.document, events, {
|
|
1455
|
+
eventSource: 'observers'
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Sets up recording for specific events fired by an emitter object.
|
|
1460
|
+
*
|
|
1461
|
+
* @param emitter The object that fires events to be recorded.
|
|
1462
|
+
* @param eventNames Array of event names to record.
|
|
1463
|
+
* @param context Additional context to include with recorded events.
|
|
1464
|
+
*/ _tapFireMethod(emitter, eventNames, context = {}) {
|
|
1465
|
+
tapObjectMethod(emitter, 'fire', {
|
|
1466
|
+
before: (callContext, [eventInfoOrName, ...params])=>{
|
|
1467
|
+
const eventName = typeof eventInfoOrName == 'string' ? eventInfoOrName : eventInfoOrName.name;
|
|
1468
|
+
if (!eventNames.includes(eventName)) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
callContext.callFrame = this._enterFrame(`${callContext.eventSource}:${eventName}`, params);
|
|
1472
|
+
return true;
|
|
1473
|
+
},
|
|
1474
|
+
after: (callContext, result)=>{
|
|
1475
|
+
this._leaveFrame(callContext.callFrame, result);
|
|
1476
|
+
},
|
|
1477
|
+
error: (callContext, error)=>{
|
|
1478
|
+
this._leaveFrame(callContext.callFrame, undefined, error);
|
|
1479
|
+
}
|
|
1480
|
+
}, context);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Triggers error callback.
|
|
1484
|
+
*/ _callErrorCallback(error) {
|
|
1485
|
+
if (!this._errorCallback || this._errors.has(error)) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
this._errors.add(error);
|
|
1489
|
+
try {
|
|
1490
|
+
// Provide a shallow copy of entries as it might be modified before error handler serializes it.
|
|
1491
|
+
this._errorCallback(error, this.getEntries());
|
|
1492
|
+
} catch (observerError) {
|
|
1493
|
+
// Silently catch observer errors to prevent them from affecting the recording.
|
|
1494
|
+
console.error('ActionsRecorder onError callback error:', observerError);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* The default handler for maxEntries callback.
|
|
1499
|
+
*/ _maxEntriesDefaultHandler() {
|
|
1500
|
+
this._entries.shift();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Creates a wrapper around a method to record its calls, results, and errors.
|
|
1505
|
+
*
|
|
1506
|
+
* @internal
|
|
1507
|
+
*
|
|
1508
|
+
* @param object The object containing the method to tap.
|
|
1509
|
+
* @param methodName The name of the method to tap.
|
|
1510
|
+
* @param tap The tap configuration with before/after/error hooks.
|
|
1511
|
+
* @param context Additional context to include with the method calls.
|
|
1512
|
+
*/ function tapObjectMethod(object, methodName, tap, context = {}) {
|
|
1513
|
+
const originalMethod = object[methodName];
|
|
1514
|
+
if (originalMethod[Symbol.for('Tapped method')]) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
object[methodName] = (...args)=>{
|
|
1518
|
+
const callContext = Object.assign({}, context);
|
|
1519
|
+
let shouldHandle;
|
|
1520
|
+
try {
|
|
1521
|
+
shouldHandle = tap.before?.(callContext, args);
|
|
1522
|
+
const result = originalMethod.apply(object, args);
|
|
1523
|
+
if (shouldHandle) {
|
|
1524
|
+
tap.after?.(callContext, result);
|
|
1525
|
+
}
|
|
1526
|
+
return result;
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
if (shouldHandle) {
|
|
1529
|
+
tap.error?.(callContext, error);
|
|
1530
|
+
}
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
object[methodName][Symbol.for('Tapped method')] = originalMethod;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Serializes a value into a JSON-serializable format.
|
|
1538
|
+
*
|
|
1539
|
+
* @internal
|
|
1540
|
+
*
|
|
1541
|
+
* @param value The value to serialize.
|
|
1542
|
+
* @param visited Set of already serialized objects to avoid circular references.
|
|
1543
|
+
* @returns A JSON-serializable representation of the value.
|
|
1544
|
+
*/ function serializeValue(value, visited = new WeakSet()) {
|
|
1545
|
+
if (!value || [
|
|
1546
|
+
'boolean',
|
|
1547
|
+
'number',
|
|
1548
|
+
'string'
|
|
1549
|
+
].includes(typeof value)) {
|
|
1550
|
+
return value;
|
|
1551
|
+
}
|
|
1552
|
+
if (typeof value.toJSON == 'function') {
|
|
1553
|
+
const jsonData = value.toJSON();
|
|
1554
|
+
// Make sure that toJSON returns plain object, otherwise it could be just a clone with circular references.
|
|
1555
|
+
if (isPlainObject(jsonData) || Array.isArray(jsonData) || [
|
|
1556
|
+
'string',
|
|
1557
|
+
'number',
|
|
1558
|
+
'boolean'
|
|
1559
|
+
].includes(typeof jsonData)) {
|
|
1560
|
+
return serializeValue(jsonData, visited);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (value instanceof Error) {
|
|
1564
|
+
return {
|
|
1565
|
+
name: value.name,
|
|
1566
|
+
message: value.message,
|
|
1567
|
+
stack: value.stack
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
// Most TypeCheckable should implement toJSON method so this is a fallback for other TypeCheckable objects.
|
|
1571
|
+
if (isTypeCheckable(value) || typeof value != 'object') {
|
|
1572
|
+
return {
|
|
1573
|
+
type: typeof value,
|
|
1574
|
+
constructor: value.constructor?.name || 'unknown',
|
|
1575
|
+
string: String(value)
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
if (value instanceof File || value instanceof Blob || value instanceof FormData || value instanceof DataTransfer) {
|
|
1579
|
+
return String(value);
|
|
1580
|
+
}
|
|
1581
|
+
if (visited.has(value)) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
visited.add(value);
|
|
1585
|
+
// Arrays.
|
|
1586
|
+
if (Array.isArray(value)) {
|
|
1587
|
+
return value.length ? value.map((item)=>serializeValue(item, visited)) : undefined;
|
|
1588
|
+
}
|
|
1589
|
+
// Other objects (plain, instances of classes, or events).
|
|
1590
|
+
const result = {};
|
|
1591
|
+
const ignoreFields = [];
|
|
1592
|
+
// DOM event additional fields.
|
|
1593
|
+
if (value.domEvent) {
|
|
1594
|
+
ignoreFields.push('domEvent', 'domTarget', 'view', 'document');
|
|
1595
|
+
result.domEvent = serializeDomEvent(value.domEvent);
|
|
1596
|
+
result.target = serializeValue(value.target);
|
|
1597
|
+
if (value.dataTransfer) {
|
|
1598
|
+
result.dataTransfer = {
|
|
1599
|
+
types: value.dataTransfer.types,
|
|
1600
|
+
htmlData: value.dataTransfer.getData('text/html'),
|
|
1601
|
+
files: serializeValue(value.dataTransfer.files)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
// Other object types.
|
|
1606
|
+
for (const [key, val] of Object.entries(value)){
|
|
1607
|
+
// Ignore private fields, DOM events serialized above, and decorated methods.
|
|
1608
|
+
if (key.startsWith('_') || ignoreFields.includes(key) || typeof val == 'function') {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const serializedValue = serializeValue(val, visited);
|
|
1612
|
+
if (serializedValue !== undefined) {
|
|
1613
|
+
result[key] = serializedValue;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (Symbol.iterator in value) {
|
|
1617
|
+
const items = Array.from(value[Symbol.iterator]()).map((item)=>serializeValue(item, visited));
|
|
1618
|
+
if (items.length) {
|
|
1619
|
+
result._items = items;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return Object.keys(result).length ? result : undefined;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Serializes a DOM event into a plain object representation.
|
|
1626
|
+
*
|
|
1627
|
+
* Extracts common properties from DOM events such as type, target information,
|
|
1628
|
+
* coordinates, key codes, and other relevant event data for debugging purposes.
|
|
1629
|
+
*
|
|
1630
|
+
* @param event The DOM event to serialize.
|
|
1631
|
+
* @returns A serialized object containing the event's key properties.
|
|
1632
|
+
*/ function serializeDomEvent(event) {
|
|
1633
|
+
let serialized = {
|
|
1634
|
+
type: event.type,
|
|
1635
|
+
target: serializeDOMTarget(event.target)
|
|
1636
|
+
};
|
|
1637
|
+
// Add mouse event properties.
|
|
1638
|
+
if (event instanceof MouseEvent) {
|
|
1639
|
+
serialized = {
|
|
1640
|
+
...serialized,
|
|
1641
|
+
button: event.button,
|
|
1642
|
+
buttons: event.buttons,
|
|
1643
|
+
ctrlKey: event.ctrlKey,
|
|
1644
|
+
shiftKey: event.shiftKey,
|
|
1645
|
+
altKey: event.altKey,
|
|
1646
|
+
metaKey: event.metaKey
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
// Add keyboard event properties.
|
|
1650
|
+
if (event instanceof KeyboardEvent) {
|
|
1651
|
+
serialized = {
|
|
1652
|
+
...serialized,
|
|
1653
|
+
key: event.key,
|
|
1654
|
+
code: event.code,
|
|
1655
|
+
keyCode: event.keyCode,
|
|
1656
|
+
ctrlKey: event.ctrlKey,
|
|
1657
|
+
shiftKey: event.shiftKey,
|
|
1658
|
+
altKey: event.altKey,
|
|
1659
|
+
metaKey: event.metaKey,
|
|
1660
|
+
repeat: event.repeat
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
// Add input event properties.
|
|
1664
|
+
if (event instanceof InputEvent) {
|
|
1665
|
+
serialized = {
|
|
1666
|
+
...serialized,
|
|
1667
|
+
data: event.data,
|
|
1668
|
+
inputType: event.inputType,
|
|
1669
|
+
isComposing: event.isComposing
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
// Add pointer event properties.
|
|
1673
|
+
if (event instanceof PointerEvent) {
|
|
1674
|
+
serialized = {
|
|
1675
|
+
...serialized,
|
|
1676
|
+
isPrimary: event.isPrimary
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Serializes a DOM event target into a plain object representation.
|
|
1681
|
+
*
|
|
1682
|
+
* @param target The DOM event target to serialize.
|
|
1683
|
+
* @returns A serialized object containing the target's information.
|
|
1684
|
+
*/ function serializeDOMTarget(target) {
|
|
1685
|
+
if (!target) {
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
if (target instanceof Element) {
|
|
1689
|
+
return {
|
|
1690
|
+
tagName: target.tagName,
|
|
1691
|
+
className: target.className,
|
|
1692
|
+
id: target.id
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
if (target instanceof Window || target instanceof Document) {
|
|
1696
|
+
return {
|
|
1697
|
+
type: target.constructor.name
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
return {};
|
|
1701
|
+
}
|
|
1702
|
+
return serialized;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Checks if a value is type-checkable, meaning it has an `is` method.
|
|
1706
|
+
*/ function isTypeCheckable(value) {
|
|
1707
|
+
return value && typeof value.is === 'function';
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
export { ActionsRecorder, ContextWatchdog, EditorWatchdog, Watchdog };
|
|
1084
1711
|
//# sourceMappingURL=index.js.map
|