From ccb7a50393810873f5566b836727a24b76b557ef Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Tue, 9 Dec 2025 15:13:19 +0100 Subject: [PATCH] feat: track execution times --- lib/TaskExecution.js | 17 ++++- lib/types.d.ts | 2 + test/TaskExecution.spec.js | 141 +++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/lib/TaskExecution.js b/lib/TaskExecution.js index 23d7b24..dd3aeff 100644 --- a/lib/TaskExecution.js +++ b/lib/TaskExecution.js @@ -58,6 +58,9 @@ export default class TaskExecution extends EventEmitter { /** @type {TaskExecutionStatus} */ this._status = 'idle'; + /** @type {number|null} */ + this._executionStartTime = null; + const eventBus = injector.get('eventBus'); eventBus.on('selection.changed', () => { @@ -78,6 +81,8 @@ export default class TaskExecution extends EventEmitter { */ async executeTask(elementId, variables) { + this._executionStartTime = Date.now(); + this._changeStatus('deploying'); const deploymentResult = await this._api.deploy(); @@ -211,10 +216,13 @@ export default class TaskExecution extends EventEmitter { elementId ); + const executionTime = this._executionStartTime ? Date.now() - this._executionStartTime : 0; + this.emit('taskExecution.finished', { success: !incident, incident, - variables + variables, + executionTime }); this.cancelTaskExecution(); @@ -238,6 +246,8 @@ export default class TaskExecution extends EventEmitter { clearInterval(this._interval); } + this._executionStartTime = null; + this._changeStatus('idle'); } @@ -249,10 +259,13 @@ export default class TaskExecution extends EventEmitter { */ _emitError(message, response) { + const executionTime = this._executionStartTime ? Date.now() - this._executionStartTime : 0; + /** @type {import('./types').TaskExecutionError} */ const error = { message, - response + response, + executionTime }; this.emit('taskExecution.error', error); diff --git a/lib/types.d.ts b/lib/types.d.ts index 84f8eff..fa911fc 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -86,11 +86,13 @@ export type TaskExecutionResult = { variables?: ElementOutputVariables; error?: TaskExecutionError; incident?: any; + executionTime: number; } export type TaskExecutionError = { message: string; response?: any; + executionTime: number; }; export type { Element, ModdleElement } from 'bpmn-js/lib/model/Types'; \ No newline at end of file diff --git a/test/TaskExecution.spec.js b/test/TaskExecution.spec.js index 23b89fa..edeb1b6 100644 --- a/test/TaskExecution.spec.js +++ b/test/TaskExecution.spec.js @@ -94,6 +94,8 @@ describe('TaskExecution', function() { } } }); + + expect(finishedSpy.firstCall.args[0]).to.have.property('executionTime').that.is.a('number'); }); @@ -204,6 +206,8 @@ describe('TaskExecution', function() { message: 'Failed to deploy process definition', response: DEPLOY_ERROR }); + + expect(errorSpy.firstCall.args[0]).to.have.property('executionTime').that.is.a('number'); }); @@ -594,6 +598,143 @@ describe('TaskExecution', function() { }); + + describe('execution time tracking', function() { + + it('should include execution time on successful task execution', async function() { + + // given + api.deploy.resolves({ success: true, response: DEFAULT_DEPLOY_RESPONSE }); + api.startInstance.resolves({ success: true, response: DEFAULT_START_INSTANCE_RESPONSE }); + api.getProcessInstance.resolves({ success: true, response: DEFAULT_GET_PROCESS_INSTANCE_RESPONSE }); + api.getProcessInstanceVariables.resolves({ success: true, response: DEFAULT_GET_PROCESS_INSTANCE_VARIABLES_RESPONSE }); + api.getProcessInstanceElementInstances.resolves({ success: true, response: DEFAULT_GET_PROCESS_INSTANCE_ELEMENT_INSTANCES_RESPONSE }); + + // when + taskExecution.executeTask('ServiceTask_1', { foo: 'bar' }); + + await clock.tickAsync(1500); + + // then + expect(finishedSpy).to.have.been.calledOnce; + + const result = finishedSpy.firstCall.args[0]; + + expect(result).to.have.property('executionTime'); + expect(result.executionTime).to.be.a('number'); + expect(result.executionTime).to.be.at.least(0); + }); + + + it('should include execution time on error', async function() { + + // given + api.deploy.resolves({ success: false, error: DEPLOY_ERROR }); + + // when + taskExecution.executeTask('ServiceTask_1', { foo: 'bar' }); + + await clock.tickAsync(500); + + // then + expect(errorSpy).to.have.been.calledOnce; + + const error = errorSpy.firstCall.args[0]; + + expect(error).to.have.property('executionTime'); + expect(error.executionTime).to.be.a('number'); + expect(error.executionTime).to.be.at.least(0); + }); + + + it('should include execution time on incident', async function() { + + // given + api.deploy.resolves({ success: true, response: DEFAULT_DEPLOY_RESPONSE }); + api.startInstance.resolves({ success: true, response: DEFAULT_START_INSTANCE_RESPONSE }); + api.getProcessInstance.resolves({ success: true, response: { + items: [ + { + 'processDefinitionId': 'Process_TaskTesting', + 'processDefinitionName': 'Process_TaskTesting', + 'processDefinitionVersion': 1, + 'startDate': '2025-09-04T12:43:52.704Z', + 'endDate': null, + 'state': 'ACTIVE', + 'hasIncident': true, + 'tenantId': '', + 'processInstanceKey': '2251799813755922', + 'processDefinitionKey': '2251799813686881' + } + ] + } }); + api.getProcessInstanceIncident.resolves({ success: true, response: { + items: [ + { + key: '2251799814592731', + processDefinitionKey: '2251799814239639', + processInstanceKey: '2251799814592711', + type: 'JOB_NO_RETRIES', + message: 'Bad gateway', + creationTime: '2025-08-21T14:40:55.402+0000', + state: 'ACTIVE', + jobKey: '2251799814592726', + tenantId: '' + } + ] + } }); + api.getProcessInstanceVariables.resolves({ success: true, response: DEFAULT_GET_PROCESS_INSTANCE_VARIABLES_RESPONSE }); + api.getProcessInstanceElementInstances.resolves({ success: true, response: DEFAULT_GET_PROCESS_INSTANCE_ELEMENT_INSTANCES_RESPONSE }); + + // when + taskExecution.executeTask('ServiceTask_1', { foo: 'bar' }); + + await clock.tickAsync(1500); + + // then + expect(finishedSpy).to.have.been.calledOnce; + + const result = finishedSpy.firstCall.args[0]; + + expect(result).to.have.property('executionTime'); + expect(result.executionTime).to.be.a('number'); + expect(result.executionTime).to.be.at.least(0); + expect(result.success).to.be.false; + }); + + + it('should reset execution start time on cancel', async function() { + + // given + api.deploy.resolves({ success: true, response: DEFAULT_DEPLOY_RESPONSE }); + api.startInstance.resolves({ success: true, response: DEFAULT_START_INSTANCE_RESPONSE }); + + // when + taskExecution.executeTask('ServiceTask_1', { foo: 'bar' }); + + await clock.tickAsync(500); + + taskExecution.cancelTaskExecution(); + + // then + expect(taskExecution._executionStartTime).to.be.null; + }); + + + it('should set execution start time when task execution starts', async function() { + + // given + api.deploy.callsFake(() => new Promise(() => {})); // Never resolves + + // when + taskExecution.executeTask('ServiceTask_1', { foo: 'bar' }); + + // then + expect(taskExecution._executionStartTime).to.be.a('number'); + }); + + }); + }); const DEFAULT_DEPLOY_RESPONSE = {