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
6 changes: 0 additions & 6 deletions src/models/maestro/process-instances.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,3 @@ export const ProcessInstanceMap: { [key: string]: string } = {
updatedAt: 'updatedTime'
};

/**
* Maps fields for Process Instance Execution History to ensure consistent naming
*/
export const ProcessInstanceExecutionHistoryMap: { [key: string]: string } = {
startTime: 'startedTime'
};
55 changes: 55 additions & 0 deletions src/models/maestro/process-instances.internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,59 @@ export interface BpmnVariableMetadata {
type: string;
elementId: string;
source: string;
}

/**
* Element run from the element-executions API response
* @internal
*/
export interface ElementExecutionRun {
status: string;
startedTimeUtc: string;
completedTimeUtc: string | null;
elementRunId: string;
workflowId: string | null;
}

/**
* Element execution from the element-executions API response
* @internal
*/
export interface ElementExecution {
completedTimeUtc: string | null;
elementId: string;
elementType: string;
elementExtensionType: string | null;
parentRunId: string | null;
parentElementId: string | null;
parentElementRunId: string | null;
runId: string;
startedTimeUtc: string;
status: string;
elementRuns: ElementExecutionRun[];
}

/**
* Top-level response from the element-executions API
* @internal
*/
export interface ElementExecutionsApiResponse {
instanceId: string;
elementExecutions: ElementExecution[];
}

/**
* Trace span from the LLMOps Traces/spans API
* @internal
*/
export interface TraceSpan {
Id: string;
TraceId: string;
ParentId: string | null;
Name: string;
StartTime: string;
EndTime: string | null;
Attributes: string | null;
ExpiryTimeUtc: string | null;
UpdatedAt: string;
}
9 changes: 6 additions & 3 deletions src/models/maestro/process-instances.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ export interface ProcessInstancesServiceModel {
/**
* Get execution history (spans) for a process instance
* @param instanceId The ID of the instance to get history for
* @param folderKey The folder key for authorization
* @returns Promise resolving to execution history
* {@link ProcessInstanceExecutionHistoryResponse}
* @example
* ```typescript
* // Get execution history for a process instance
* const history = await processInstances.getExecutionHistory(
* <instanceId>
* <instanceId>,
* <folderKey>
* );
*
* // Analyze execution timeline
Expand All @@ -113,7 +115,7 @@ export interface ProcessInstancesServiceModel {
* });
* ```
*/
getExecutionHistory(instanceId: string): Promise<ProcessInstanceExecutionHistoryResponse[]>;
getExecutionHistory(instanceId: string, folderKey: string): Promise<ProcessInstanceExecutionHistoryResponse[]>;

/**
* Get BPMN XML file for a process instance
Expand Down Expand Up @@ -355,8 +357,9 @@ function createProcessInstanceMethods(instanceData: RawProcessInstanceGetRespons

async getExecutionHistory(): Promise<ProcessInstanceExecutionHistoryResponse[]> {
if (!instanceData.instanceId) throw new Error('Process instance ID is undefined');
if (!instanceData.folderKey) throw new Error('Process instance folder key is undefined');

return service.getExecutionHistory(instanceData.instanceId);
return service.getExecutionHistory(instanceData.instanceId, instanceData.folderKey);
},

async getBpmn(): Promise<BpmnXmlString> {
Expand Down
2 changes: 1 addition & 1 deletion src/models/maestro/process-instances.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface ProcessInstanceExecutionHistoryResponse {
startedTime: string;
endTime: string | null;
attributes: string | null;
createdTime: string;
createdTime?: string;
updatedTime?: string;
expiredTime: string | null;
// TO Do: Add status and attributes interface
Expand Down
60 changes: 54 additions & 6 deletions src/services/maestro/processes/process-instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import { MAESTRO_ENDPOINTS } from '../../../utils/constants/endpoints';
import { createHeaders } from '../../../utils/http/headers';
import { FOLDER_KEY, CONTENT_TYPES } from '../../../utils/constants/headers';
import { transformData } from '../../../utils/transform';
import { ProcessInstanceMap, ProcessInstanceExecutionHistoryMap } from '../../../models/maestro/process-instances.constants';
import { ProcessInstanceMap } from '../../../models/maestro/process-instances.constants';
import { BpmnXmlString } from '../../../models/maestro/process-instances.types';
import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../../utils/pagination';
import { PaginationHelpers } from '../../../utils/pagination/helpers';
import { PaginationType } from '../../../utils/pagination/internal-types';
import { PROCESS_INSTANCE_PAGINATION, PROCESS_INSTANCE_TOKEN_PARAMS } from '../../../utils/constants/common';
import { track } from '../../../core/telemetry';
import { BpmnVariableMetadata } from '../../../models/maestro/process-instances.internal-types';
import { BpmnVariableMetadata, ElementExecutionsApiResponse, TraceSpan } from '../../../models/maestro/process-instances.internal-types';


export class ProcessInstancesService extends BaseService implements ProcessInstancesServiceModel {
Expand Down Expand Up @@ -119,14 +119,62 @@ export class ProcessInstancesService extends BaseService implements ProcessInsta
/**
* Get execution history (spans) for a process instance
* @param instanceId The ID of the instance to get history for
* @param folderKey The folder key for authorization
* @returns Promise<ProcessInstanceExecutionHistoryResponse[]>
*/
@track('ProcessInstances.GetExecutionHistory')
async getExecutionHistory(instanceId: string): Promise<ProcessInstanceExecutionHistoryResponse[]> {
const response = await this.get<ProcessInstanceExecutionHistoryResponse[]>(MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId));
return response.data.map(historyItem =>
transformData(historyItem, ProcessInstanceExecutionHistoryMap)
async getExecutionHistory(instanceId: string, folderKey: string): Promise<ProcessInstanceExecutionHistoryResponse[]> {
// Call element-executions API to get structural BPMN data and traceId
const elementExecResponse = await this.get<ElementExecutionsApiResponse>(
MAESTRO_ENDPOINTS.INSTANCES.GET_ELEMENT_EXECUTIONS(instanceId),
{
headers: createHeaders({ [FOLDER_KEY]: folderKey })
}
);

const traceId = elementExecResponse.data.instanceId;

// Call spans API with traceId to get trace/span details
const spansResponse = await this.get<TraceSpan[]>(
MAESTRO_ENDPOINTS.TRACES.GET_SPANS(traceId),
{
headers: createHeaders({ [FOLDER_KEY]: folderKey })
}
);

// Create span lookup by Id for merging
const spanMap = new Map<string, TraceSpan>();
for (const span of spansResponse.data) {
spanMap.set(span.Id, span);
}

// Merge: for each elementRun, find matching span and map to response type
const results: ProcessInstanceExecutionHistoryResponse[] = [];

for (const elementExec of elementExecResponse.data.elementExecutions) {
for (const run of elementExec.elementRuns) {
const span = spanMap.get(run.elementRunId);
if (span) {
results.push(this.mapSpanToHistory(span));
}
}
}

return results;
}

private mapSpanToHistory(span: TraceSpan): ProcessInstanceExecutionHistoryResponse {
return {
id: span.Id,
traceId: span.TraceId,
parentId: span.ParentId,
name: span.Name,
startedTime: span.StartTime,
endTime: span.EndTime,
attributes: span.Attributes,
updatedTime: span.UpdatedAt,
expiredTime: span.ExpiryTimeUtc,
};
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants/endpoints/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export const PIMS_BASE = 'pims_';
export const DATAFABRIC_BASE = 'datafabric_';
export const IDENTITY_BASE = 'identity_';
export const AUTOPILOT_BASE = 'autopilotforeveryone_';
export const LLMOPS_BASE = 'llmopstenant_';
7 changes: 5 additions & 2 deletions src/utils/constants/endpoints/maestro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Maestro Service Endpoints
*/

import { PIMS_BASE } from './base';
import { PIMS_BASE, LLMOPS_BASE } from './base';

/**
* Maestro Process Service Endpoints
Expand All @@ -15,7 +15,7 @@ export const MAESTRO_ENDPOINTS = {
INSTANCES: {
GET_ALL: `${PIMS_BASE}/api/v1/instances`,
GET_BY_ID: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}`,
GET_EXECUTION_HISTORY: (instanceId: string) => `${PIMS_BASE}/api/v1/spans/${instanceId}`,
GET_ELEMENT_EXECUTIONS: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/element-executions`,
GET_BPMN: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/bpmn`,
GET_VARIABLES: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/variables`,
CANCEL: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/cancel`,
Expand All @@ -27,6 +27,9 @@ export const MAESTRO_ENDPOINTS = {
GET_BY_PROCESS: (processKey: string) => `${PIMS_BASE}/api/v1/incidents/process/${processKey}`,
GET_BY_INSTANCE: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/incidents`,
},
TRACES: {
GET_SPANS: (traceId: string) => `${LLMOPS_BASE}/api/Traces/spans?traceId=${traceId}`,
},
CASES: {
GET_CASE_JSON: (instanceId: string) => `${PIMS_BASE}/api/v1/cases/${instanceId}/case-json`,
GET_ELEMENT_EXECUTIONS: (instanceId: string) => `${PIMS_BASE}/api/v1/element-executions/case-instances/${instanceId}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ describe.each(modes)('Maestro Process Instances - Integration Tests [%s]', (mode
const { processInstances } = getServices();

try {
const result = await processInstances.getExecutionHistory(testInstanceId);
const result = await processInstances.getExecutionHistory(testInstanceId, config.folderId || '');

expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
Expand Down
22 changes: 16 additions & 6 deletions tests/unit/models/maestro/process-instances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,20 @@ describe('Process Instance Models', () => {
});

describe('processInstance.getExecutionHistory()', () => {
it('should call processInstance.getExecutionHistory with bound instanceId', async () => {
it('should call processInstance.getExecutionHistory with bound instanceId and folderKey', async () => {
const mockInstanceData = createMockProcessInstance();
const instance = createProcessInstanceWithMethods(mockInstanceData, mockService);

const mockHistory = [createMockExecutionHistory()];
mockService.getExecutionHistory = vi.fn().mockResolvedValue(mockHistory);


const result = await instance.getExecutionHistory();


expect(mockService.getExecutionHistory).toHaveBeenCalledWith(
MAESTRO_TEST_CONSTANTS.INSTANCE_ID
MAESTRO_TEST_CONSTANTS.INSTANCE_ID,
MAESTRO_TEST_CONSTANTS.FOLDER_KEY
);
expect(result).toEqual(mockHistory);
});
Expand All @@ -252,9 +253,18 @@ describe('Process Instance Models', () => {
const invalidInstanceData = { ...mockInstanceData, instanceId: undefined as any };
const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService);


await expect(invalidInstance.getExecutionHistory()).rejects.toThrow('Process instance ID is undefined');
});

it('should throw error if folderKey is undefined', async () => {
const mockInstanceData = createMockProcessInstance();
const invalidInstanceData = { ...mockInstanceData, folderKey: undefined as any };
const invalidInstance = createProcessInstanceWithMethods(invalidInstanceData, mockService);


await expect(invalidInstance.getExecutionHistory()).rejects.toThrow('Process instance folder key is undefined');
});
});

describe('processInstance.getBpmn()', () => {
Expand Down
57 changes: 45 additions & 12 deletions tests/unit/services/maestro/process-instances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
TEST_CONSTANTS,
createMockProcessInstance,
createMockBpmnWithVariables,
createMockExecutionHistory,
createMockElementExecutionsResponse,
createMockTraceSpan,
createMockProcessVariables,
createMockMaestroApiOperationResponse
} from '../../../utils/mocks';
Expand All @@ -20,7 +21,7 @@
ProcessInstanceOperationOptions,
ProcessInstanceGetVariablesOptions,
RawProcessInstanceGetResponse,
ProcessInstanceExecutionHistoryResponse

Check warning on line 24 in tests/unit/services/maestro/process-instances.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'ProcessInstanceExecutionHistoryResponse'.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-typescript&issues=AZzSaOQiMh7oXKOVNACK&open=AZzSaOQiMh7oXKOVNACK&pullRequest=270
} from '../../../../src/models/maestro';

// ===== MOCKING =====
Expand Down Expand Up @@ -208,33 +209,65 @@

describe('getExecutionHistory', () => {
it('should return execution history for process instance', async () => {

const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID;
const mockApiResponse: ProcessInstanceExecutionHistoryResponse[] = [createMockExecutionHistory()];
const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY;

mockApiClient.get.mockResolvedValue(mockApiResponse);
mockApiClient.get
.mockResolvedValueOnce(createMockElementExecutionsResponse())
.mockResolvedValueOnce([createMockTraceSpan()]);


const result = await service.getExecutionHistory(instanceId);
const result = await service.getExecutionHistory(instanceId, folderKey);

expect(mockApiClient.get).toHaveBeenCalledWith(
MAESTRO_ENDPOINTS.INSTANCES.GET_ELEMENT_EXECUTIONS(instanceId),
{
headers: expect.objectContaining({
[FOLDER_KEY]: folderKey
})
}
);


expect(mockApiClient.get).toHaveBeenCalledWith(
MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId),
{}
MAESTRO_ENDPOINTS.TRACES.GET_SPANS(instanceId),
Comment thread
vnaren23 marked this conversation as resolved.
{
headers: expect.objectContaining({
[FOLDER_KEY]: folderKey
})
}
);

expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty('id', MAESTRO_TEST_CONSTANTS.SPAN_ID);
expect(result[0]).toHaveProperty('traceId', MAESTRO_TEST_CONSTANTS.TRACE_ID);
expect(result[0]).toHaveProperty('name', MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME);
Comment thread
vnaren23 marked this conversation as resolved.
});

it('should only include spans matched to elementRuns', async () => {
const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID;
const folderKey = MAESTRO_TEST_CONSTANTS.FOLDER_KEY;
const unmatchedSpan = createMockTraceSpan({
Id: 'nested-agent-span-1',
ParentId: MAESTRO_TEST_CONSTANTS.SPAN_ID,
Name: 'LangGraph',
Attributes: null
});

mockApiClient.get
.mockResolvedValueOnce(createMockElementExecutionsResponse())
.mockResolvedValueOnce([createMockTraceSpan(), unmatchedSpan]);

const result = await service.getExecutionHistory(instanceId, folderKey);

// Only the matched elementRun span should be included
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty('id', MAESTRO_TEST_CONSTANTS.SPAN_ID);
});

it('should handle API errors', async () => {

const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE);
mockApiClient.get.mockRejectedValue(error);


await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
});
});

Expand Down
Loading
Loading