Skip to content

Commit 285cbf2

Browse files
Show heartbeat info for started activities (#974)
* Show heartbeat details and last heartbeat time for activities that have been started by retrieving the info from pendingActivityTaskStartEvent (which exists but is hidden after the actual activity task started event is available) * Populate heartbeat info in additionalDetails * Move the logic to hide pending event from groupHistoryEvents to getActivityGroupFromEvents * Display heartbeatDetails as JSON
1 parent 673fba3 commit 285cbf2

12 files changed

+197
-13
lines changed

src/views/workflow-history/config/workflow-history-event-details.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const workflowHistoryEventDetailsConfig = [
5252
{
5353
name: 'Json as PrettyJson',
5454
pathRegex:
55-
'(input|result|details|failureDetails|Error|lastCompletionResult)$',
55+
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails)$',
5656
valueComponent: WorkflowHistoryEventDetailsJson,
5757
forceWrap: true,
5858
},

src/views/workflow-history/helpers/get-common-history-group-fields.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type HistoryEventsGroup,
88
type HistoryGroupEventToStatusMap,
99
type HistoryGroupEventToStringMap,
10+
type HistoryGroupEventToAdditionalDetailsMap,
1011
} from '../workflow-history.types';
1112

1213
export default function getCommonHistoryGroupFields<
@@ -17,7 +18,8 @@ export default function getCommonHistoryGroupFields<
1718
eventToLabelMap: HistoryGroupEventToStringMap<GroupT>,
1819
eventToTimeLabelPrefixMap: Partial<HistoryGroupEventToStringMap<GroupT>>,
1920
closeEvent: GroupT['events'][number] | null | undefined,
20-
eventStatusToNegativeFieldsMap?: HistoryGroupEventStatusToNegativeFieldsMap<GroupT>
21+
eventStatusToNegativeFieldsMap?: HistoryGroupEventStatusToNegativeFieldsMap<GroupT>,
22+
eventToAdditionalDetailsMap?: HistoryGroupEventToAdditionalDetailsMap<GroupT>
2123
): Pick<
2224
GroupT,
2325
| 'eventsMetadata'
@@ -42,13 +44,15 @@ export default function getCommonHistoryGroupFields<
4244
: `${eventToLabelMap[attrs]} at`;
4345

4446
const negativeFields = eventStatusToNegativeFieldsMap?.[attrs];
47+
const additionalDetails = eventToAdditionalDetailsMap?.[attrs];
4548

4649
return {
4750
label: eventToLabelMap[attrs],
4851
status: eventStatus,
4952
timeMs,
5053
timeLabel: timeMs ? `${prefix} ${formatDate(timeMs)}` : '',
5154
...(negativeFields?.length ? { negativeFields } : {}),
55+
...(additionalDetails ? { additionalDetails } : {}),
5256
};
5357
});
5458

src/views/workflow-history/helpers/get-history-group-from-events/__tests__/get-activity-group-from-events.test.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ describe('getActivityGroupFromEvents', () => {
152152
it('should return group eventsMetadata with correct labels', () => {
153153
const events: ExtendedActivityHistoryEvent[] = [
154154
scheduleActivityTaskEvent,
155-
pendingActivityTaskStartEvent,
156155
startActivityTaskEvent,
157156
completeActivityTaskEvent,
158157
failedActivityTaskEvent,
@@ -162,7 +161,6 @@ describe('getActivityGroupFromEvents', () => {
162161
const group = getActivityGroupFromEvents(events);
163162
expect(group.eventsMetadata.map(({ label }) => label)).toEqual([
164163
'Scheduled',
165-
'Starting',
166164
'Started',
167165
'Completed',
168166
'Failed',
@@ -389,4 +387,113 @@ describe('getActivityGroupFromEvents', () => {
389387
expect(metadata.negativeFields).toBeUndefined();
390388
});
391389
});
390+
391+
it('should include heartbeat details in additionalDetails when pending activity start event is present', () => {
392+
const events: ExtendedActivityHistoryEvent[] = [
393+
scheduleActivityTaskEvent,
394+
pendingActivityTaskStartEvent,
395+
startActivityTaskEvent,
396+
];
397+
const group = getActivityGroupFromEvents(events);
398+
399+
// The started event should have additionalDetails with heartbeat information
400+
const startedEventMetadata = group.eventsMetadata.find(
401+
(metadata) => metadata.label === 'Started'
402+
);
403+
expect(startedEventMetadata?.additionalDetails).toEqual({
404+
heartbeatDetails: [
405+
'1725747370575409843',
406+
'gadence-canary-xdc',
407+
'workflow.sanity',
408+
],
409+
lastHeartbeatTime: null,
410+
});
411+
412+
// Other events should not have additionalDetails
413+
const otherEventsMetadata = group.eventsMetadata.filter(
414+
(metadata) => metadata.label !== 'Started'
415+
);
416+
otherEventsMetadata.forEach((metadata) => {
417+
expect(metadata.additionalDetails).toBeUndefined();
418+
});
419+
});
420+
421+
it('should include last heartbeat time when pending activity start event has lastHeartbeatTime', () => {
422+
const pendingEventWithHeartbeatTime = {
423+
...pendingActivityTaskStartEvent,
424+
pendingActivityTaskStartEventAttributes: {
425+
...pendingActivityTaskStartEvent.pendingActivityTaskStartEventAttributes,
426+
lastHeartbeatTime: {
427+
seconds: '1725747370',
428+
nanos: 599547391,
429+
},
430+
},
431+
};
432+
433+
const events: ExtendedActivityHistoryEvent[] = [
434+
scheduleActivityTaskEvent,
435+
pendingEventWithHeartbeatTime,
436+
startActivityTaskEvent,
437+
];
438+
const group = getActivityGroupFromEvents(events);
439+
440+
const startedEventMetadata = group.eventsMetadata.find(
441+
(metadata) => metadata.label === 'Started'
442+
);
443+
expect(startedEventMetadata?.additionalDetails).toEqual({
444+
heartbeatDetails: [
445+
'1725747370575409843',
446+
'gadence-canary-xdc',
447+
'workflow.sanity',
448+
],
449+
lastHeartbeatTime: new Date('2024-09-07T22:16:10.599Z'),
450+
});
451+
});
452+
453+
it('should not include additionalDetails when no pending activity start event is present', () => {
454+
const events: ExtendedActivityHistoryEvent[] = [
455+
scheduleActivityTaskEvent,
456+
startActivityTaskEvent,
457+
completeActivityTaskEvent,
458+
];
459+
const group = getActivityGroupFromEvents(events);
460+
461+
// No events should have additionalDetails
462+
group.eventsMetadata.forEach((metadata) => {
463+
expect(metadata.additionalDetails).toBeUndefined();
464+
});
465+
});
466+
467+
it('should filter out pending activity start events when activity start event is present', () => {
468+
const events: ExtendedActivityHistoryEvent[] = [
469+
scheduleActivityTaskEvent,
470+
pendingActivityTaskStartEvent,
471+
startActivityTaskEvent,
472+
completeActivityTaskEvent,
473+
];
474+
const group = getActivityGroupFromEvents(events);
475+
476+
// Should only have 3 events (schedule, start, complete) - pending start should be filtered out
477+
expect(group.eventsMetadata).toHaveLength(3);
478+
expect(group.eventsMetadata.map(({ label }) => label)).toEqual([
479+
'Scheduled',
480+
'Started',
481+
'Completed',
482+
]);
483+
});
484+
485+
it('should include pending activity start events when only scheduled and pending events are present', () => {
486+
const events: ExtendedActivityHistoryEvent[] = [
487+
scheduleActivityTaskEvent,
488+
pendingActivityTaskStartEvent,
489+
];
490+
const group = getActivityGroupFromEvents(events);
491+
492+
// Should have both events when no activity start event is present
493+
expect(group.eventsMetadata).toHaveLength(2);
494+
expect(group.eventsMetadata.map(({ label }) => label)).toEqual([
495+
'Scheduled',
496+
'Starting',
497+
]);
498+
});
392499
});

src/views/workflow-history/helpers/get-history-group-from-events/get-activity-group-from-events.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import formatPayload from '@/utils/data-formatters/format-payload';
2+
import formatTimestampToDatetime from '@/utils/data-formatters/format-timestamp-to-datetime';
3+
14
import WORKFLOW_HISTORY_SHOULD_SHORTEN_GROUP_LABELS_CONFIG from '../../config/workflow-history-should-shorten-group-labels.config';
25
import type {
36
ActivityHistoryGroup,
47
ExtendedActivityHistoryEvent,
58
HistoryGroupEventStatusToNegativeFieldsMap,
9+
HistoryGroupEventToAdditionalDetailsMap,
610
HistoryGroupEventToStatusMap,
711
HistoryGroupEventToStringMap,
812
PendingActivityTaskStartEvent,
@@ -129,19 +133,50 @@ export default function getActivityGroupFromEvents(
129133
? 'Last started at'
130134
: 'Scheduled at';
131135

136+
const eventToAdditionalDetails: HistoryGroupEventToAdditionalDetailsMap<ActivityHistoryGroup> =
137+
{
138+
...(pendingStartEvent
139+
? {
140+
activityTaskStartedEventAttributes: {
141+
heartbeatDetails: formatPayload(
142+
pendingStartEvent.pendingActivityTaskStartEventAttributes
143+
.heartbeatDetails
144+
),
145+
lastHeartbeatTime: formatTimestampToDatetime(
146+
pendingStartEvent.pendingActivityTaskStartEventAttributes
147+
.lastHeartbeatTime
148+
),
149+
},
150+
}
151+
: {}),
152+
};
153+
154+
const shouldShowPendingEvent = Boolean(
155+
scheduleEvent &&
156+
pendingStartEvent &&
157+
!(startEvent || closeEvent || timeoutEvent)
158+
);
159+
160+
const finalEvents = shouldShowPendingEvent
161+
? events
162+
: events.filter(
163+
(e) => e.attributes !== 'pendingActivityTaskStartEventAttributes'
164+
);
165+
132166
return {
133167
label,
134168
shortLabel,
135169
hasMissingEvents,
136170
groupType,
137171
badges,
138172
...getCommonHistoryGroupFields<ActivityHistoryGroup>(
139-
events,
173+
finalEvents,
140174
eventToStatus,
141175
eventToLabel,
142176
{ pendingActivityTaskStartEventAttributes: pendingStartEventTimePrefix },
143177
closeEvent || timeoutEvent,
144-
eventStatusToNegativeFields
178+
eventStatusToNegativeFields,
179+
eventToAdditionalDetails
145180
),
146181
};
147182
}

src/views/workflow-history/helpers/group-history-events.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,9 @@ export function groupHistoryEvents(
115115
);
116116
} else {
117117
const currentGroup = groupByFirstEventId[groupId];
118-
// add pendingStart to group only if it is scheduled
119118
if (
120119
pa.eventTime &&
121120
currentGroup &&
122-
currentGroup?.events.length === 1 &&
123-
currentGroup.events[0].attributes ===
124-
'activityTaskScheduledEventAttributes' &&
125121
currentGroup.events.every(isExtendedActivityEvent)
126122
) {
127123
const updatedEventsArr: ExtendedActivityHistoryEvent[] = [

src/views/workflow-history/workflow-history-event-details/__tests__/workflow-history-event-details.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,29 @@ describe(WorkflowHistoryEventDetails.name, () => {
138138
negativeFields,
139139
});
140140
});
141+
142+
it('passes additionalDetails merged with formatted event details to generateHistoryEventDetails', () => {
143+
const additionalDetails = {
144+
key1: 'value1',
145+
key2: {
146+
value: 2,
147+
},
148+
};
149+
mockGenerateHistoryEventDetails.mockReturnValue([]);
150+
151+
render(
152+
<WorkflowHistoryEventDetails
153+
event={completeActivityTaskEvent}
154+
decodedPageUrlParams={workflowPageUrlParams}
155+
additionalDetails={additionalDetails}
156+
/>
157+
);
158+
159+
expect(mockGenerateHistoryEventDetails).toHaveBeenCalledWith({
160+
details: {
161+
mockFormatted: true,
162+
...additionalDetails,
163+
},
164+
});
165+
});
141166
});

src/views/workflow-history/workflow-history-event-details/workflow-history-event-details.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function WorkflowHistoryEventDetails({
1616
event,
1717
decodedPageUrlParams,
1818
negativeFields,
19+
additionalDetails,
1920
}: Props) {
2021
const { cls } = useStyletronClasses(cssStyles);
2122

@@ -25,9 +26,15 @@ export default function WorkflowHistoryEventDetails({
2526
: formatWorkflowHistoryEvent(event);
2627

2728
return result
28-
? generateHistoryEventDetails({ details: result, negativeFields })
29+
? generateHistoryEventDetails({
30+
details: {
31+
...result,
32+
...additionalDetails,
33+
},
34+
negativeFields,
35+
})
2936
: [];
30-
}, [event, negativeFields]);
37+
}, [event, negativeFields, additionalDetails]);
3138

3239
if (detailsEntries.length === 0)
3340
return <div className={cls.emptyDetails}>No Details</div>;

src/views/workflow-history/workflow-history-event-details/workflow-history-event-details.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
export type Props = {
99
event: ExtendedHistoryEvent;
1010
negativeFields?: Array<string>;
11+
additionalDetails?: Record<string, any>;
1112
decodedPageUrlParams: WorfklowHistoryProps['params'];
1213
};
1314

src/views/workflow-history/workflow-history-events-card/workflow-history-events-card.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export default function WorkflowHistoryEventsCard({
6868
event={event}
6969
decodedPageUrlParams={decodedPageUrlParams}
7070
negativeFields={eventMetadata.negativeFields}
71+
additionalDetails={eventMetadata.additionalDetails}
7172
/>
7273
</Panel>
7374
);

src/views/workflow-history/workflow-history-events-card/workflow-history-events-card.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type Props = {
1212
events: ExtendedHistoryEvent[];
1313
eventsMetadata: Pick<
1414
HistoryGroupEventMetadata,
15-
'label' | 'status' | 'negativeFields'
15+
'label' | 'status' | 'negativeFields' | 'additionalDetails'
1616
>[];
1717
showEventPlaceholder?: boolean;
1818
decodedPageUrlParams: WorfklowHistoryProps['params'];

0 commit comments

Comments
 (0)