Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions lib/TaskExecution.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -238,6 +246,8 @@ export default class TaskExecution extends EventEmitter {
clearInterval(this._interval);
}

this._executionStartTime = null;

this._changeStatus('idle');
}

Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions lib/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
141 changes: 141 additions & 0 deletions test/TaskExecution.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ describe('TaskExecution', function() {
}
}
});

expect(finishedSpy.firstCall.args[0]).to.have.property('executionTime').that.is.a('number');
});


Expand Down Expand Up @@ -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');
});


Expand Down Expand Up @@ -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': '<default>',
'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: '<default>'
}
]
} });
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 = {
Expand Down