Skip to content

Commit 52d4d5c

Browse files
committed
fix(call-recorder): make the recording switch cancel asynchronously and gate every bot creation
Addresses the review on the switch. The sync route no longer walks open recordings one at a time calling Recall inline, which risked the 900s timeout and hammered a rate-limited API from a request the settings toggle waits on. It now flips every open request to canceled in bulk, reusing the batched update the uninstall hook already uses, and enqueues cancel-scheduled-recall-bots for the slow part. That job deletes bots with bounded concurrency, works in bounded slices, and re-enqueues itself only when a run actually freed a bot, so a Recall outage stops the chain instead of spinning. The daily cancellation retry stays the backstop it already was, so no new cron is needed. The policy gate only covered reconciliation, so a request whose cancellation failed could still be picked up by the pending-request recovery cron and given a bot with the switch off. The check now sits in scheduleRecallBotForCallRecording, the single place a bot is created, which closes every recovery path at once. On the front end a failed save reverted nothing, so the tab would hide every setting and claim recording was paused while it was still running; the switch now rolls back on failure. The save queue also awaits the sync, so toggling off and straight back on can no longer run two syncs concurrently. Docs now use the switch's real label.
1 parent 8c25e26 commit 52d4d5c

25 files changed

Lines changed: 255 additions & 118 deletions

packages/twenty-apps/public/call-recorder/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ variable to `false` to turn summaries off.
2424

2525
## 🗓️ Pausing the recorder
2626

27-
The **Send bot to all my calendar meetings** toggle in the app settings is on
27+
The **Record my calendar meetings** toggle in the app settings is on
2828
by default. Turn it off to stop scheduling bots for upcoming meetings and cancel
2929
every recording that is already scheduled. Turn it back on and the app sweeps
3030
upcoming meetings to schedule bots again.

packages/twenty-apps/public/call-recorder/SETUP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Set these on the application registration after installing
3636
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
3737

3838
> **Calendar scheduling** (`CALL_RECORDER_CALENDAR_BOT_SCHEDULING_ENABLED`,
39-
> the "Send bot to all my calendar meetings" toggle; turning it off cancels
39+
> the "Record my calendar meetings" toggle; turning it off cancels
4040
> every scheduled recording), **bot behavior settings** (display name,
4141
> recording notice, join timing, lobby and leave timeouts), the transcription
4242
> provider

packages/twenty-apps/public/call-recorder/src/__tests__/call-recorder-lifecycle.integration-test.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { reconcileCallRecorderForCalendarEventIds } from 'src/logic-functions/fl
1616
import { retryFailedRecallCancellations } from 'src/logic-functions/flows/retry-failed-recall-cancellations.util';
1717
import { scheduleRecallBotsForPendingCallRecordings } from 'src/logic-functions/flows/schedule-recall-bots-for-pending-call-recordings.util';
1818
import { processRecallWebhookHandler } from 'src/logic-functions/process-recall-webhook';
19+
import { cancelScheduledRecallBotsHandler } from 'src/logic-functions/cancel-scheduled-recall-bots';
1920
import { syncCalendarBotSchedulingHandler } from 'src/logic-functions/sync-calendar-bot-scheduling';
2021
import { CALL_RECORDER_CALENDAR_BOT_SCHEDULING_ENABLED_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-calendar-bot-scheduling-enabled-env-var-name';
2122

@@ -924,25 +925,45 @@ describe('call recorder app lifecycle (integration)', () => {
924925
'false',
925926
);
926927

927-
it('cancels every scheduled recording and its bot when turned off', async () => {
928+
it('cancels the request inline and leaves the Recall bot to the enqueued job', async () => {
928929
const { callRecordingId, botId } =
929930
await scheduleRecordingThroughCalendarReconciliation();
930931

931932
turnRecordingOff();
932933

933934
const result = await syncCalendarBotSchedulingHandler();
935+
936+
expect(result).toEqual(
937+
expect.objectContaining({ outcome: 'scheduled-bots-canceled' }),
938+
);
939+
expect(
940+
(await fetchCallRecording(callRecordingId)).recordingRequestStatus,
941+
).toBe('CANCELED');
942+
// The toggle must not wait on Recall, so the bot is still alive here.
943+
expect(recall.deletedBotIds).not.toContain(botId);
944+
945+
await cancelScheduledRecallBotsHandler();
946+
934947
const callRecording = await fetchCallRecording(callRecordingId);
935948

936-
expect(callRecording.recordingRequestStatus).toBe('CANCELED');
937949
expect(callRecording.externalBotId).toBeFalsy();
938950
expect(recall.deletedBotIds).toContain(botId);
939-
expect(result).toEqual(
940-
expect.objectContaining({
941-
outcome: 'scheduled-bots-canceled',
942-
canceledCallRecordingIds: expect.arrayContaining([callRecordingId]),
943-
failedCallRecordingIds: [],
944-
}),
945-
);
951+
});
952+
953+
it('does not schedule a bot for a request the cancellation missed', async () => {
954+
const calendarEventId = await createCalendarEvent();
955+
const callRecordingId = await createPendingCallRecording({
956+
calendarEventId,
957+
});
958+
959+
turnRecordingOff();
960+
961+
await runPendingRecoveryCron();
962+
963+
const callRecording = await fetchCallRecording(callRecordingId);
964+
965+
expect(callRecording.externalBotId).toBeFalsy();
966+
expect(recall.botForCallRecording(callRecordingId)).toBeUndefined();
946967
});
947968

948969
it('schedules nothing for an upcoming meeting while turned off', async () => {

packages/twenty-apps/public/call-recorder/src/constants/universal-identifiers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export const CALL_RECORDING_TRANSCRIPT_IMPORT_CLAIMED_AT_FIELD_UNIVERSAL_IDENTIF
7070
export const CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER =
7171
'bb9523d3-457e-4f4b-8c79-27a77afb87da';
7272

73+
export const CANCEL_SCHEDULED_RECALL_BOTS_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
74+
'a1b0f097-edeb-45a3-a53e-bf666cbfe3a4';
75+
7376
export const CANCEL_RECALL_BOT_ON_CALL_RECORDING_DELETE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
7477
'84981386-881f-4123-a2dd-82e0932ba663';
7578

packages/twenty-apps/public/call-recorder/src/front-components/components/SchedulingSection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export const SchedulingSection = ({
2121
frontComponentId,
2222
variableKey: CALL_RECORDER_CALENDAR_BOT_SCHEDULING_ROW.variableKey,
2323
onSaveSuccess: () => requestCalendarBotSchedulingSync(),
24+
// A failed save would otherwise leave the tab hiding every setting while
25+
// the recorder is in fact still running.
26+
onSaveError: (value) => onEnabledChange(value !== 'true'),
2427
});
2528

2629
const handleChange = (checked: boolean) => {

packages/twenty-apps/public/call-recorder/src/front-components/components/Toggle.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ const TOGGLE_DIMENSIONS_BY_SIZE: Record<ToggleSize, ToggleDimensions> = {
2020

2121
const THUMB_INSET_PIXELS = 2;
2222

23-
const getCheckedThumbOffsetPixels = ({
24-
width,
25-
thumbSize,
26-
}: ToggleDimensions) => width - thumbSize - THUMB_INSET_PIXELS;
23+
const getCheckedThumbOffsetPixels = ({ width, thumbSize }: ToggleDimensions) =>
24+
width - thumbSize - THUMB_INSET_PIXELS;
2725

2826
const StyledToggle = styled.button<{
2927
$toggleSize: ToggleSize;
@@ -37,7 +35,8 @@ const StyledToggle = styled.button<{
3735
cursor: pointer;
3836
display: flex;
3937
flex-shrink: 0;
40-
height: ${({ $toggleSize }) => TOGGLE_DIMENSIONS_BY_SIZE[$toggleSize].height}px;
38+
height: ${({ $toggleSize }) =>
39+
TOGGLE_DIMENSIONS_BY_SIZE[$toggleSize].height}px;
4140
padding: 0;
4241
position: relative;
4342
transition: background-color
@@ -75,9 +74,7 @@ const StyledThumb = styled.span<{
7574
&[data-checked='true'] {
7675
transform: translate(
7776
${({ $toggleSize }) =>
78-
getCheckedThumbOffsetPixels(
79-
TOGGLE_DIMENSIONS_BY_SIZE[$toggleSize],
80-
)}px,
77+
getCheckedThumbOffsetPixels(TOGGLE_DIMENSIONS_BY_SIZE[$toggleSize])}px,
8178
-50%
8279
);
8380
}

packages/twenty-apps/public/call-recorder/src/front-components/hooks/use-autosave-application-variable.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,26 @@ import { createApplicationVariableSaveQueue } from 'src/front-components/utils/c
99
type UseAutosaveApplicationVariableParams = {
1010
frontComponentId: string;
1111
variableKey: string;
12-
onSaveSuccess?: (value: string) => void;
12+
onSaveSuccess?: (value: string) => void | Promise<void>;
13+
onSaveError?: (value: string) => void;
1314
};
1415

1516
export const useAutosaveApplicationVariable = ({
1617
frontComponentId,
1718
variableKey,
1819
onSaveSuccess,
20+
onSaveError,
1921
}: UseAutosaveApplicationVariableParams) => {
2022
const { saveApplicationVariable } =
2123
useSaveApplicationVariable(frontComponentId);
2224
const saveApplicationVariableRef = useRef(saveApplicationVariable);
2325
const onSaveSuccessRef = useRef(onSaveSuccess);
26+
const onSaveErrorRef = useRef(onSaveError);
2427
const variableKeyRef = useRef(variableKey);
2528

2629
saveApplicationVariableRef.current = saveApplicationVariable;
2730
onSaveSuccessRef.current = onSaveSuccess;
31+
onSaveErrorRef.current = onSaveError;
2832
variableKeyRef.current = variableKey;
2933

3034
const saveQueueRef = useRef<
@@ -40,8 +44,14 @@ export const useAutosaveApplicationVariable = ({
4044
});
4145

4246
if (isSaved) {
43-
onSaveSuccessRef.current?.(value);
47+
// Awaited so the queue serializes the follow-up work of consecutive
48+
// saves; two toggles in a row must not sync concurrently.
49+
await onSaveSuccessRef.current?.(value);
50+
51+
return;
4452
}
53+
54+
onSaveErrorRef.current?.(value);
4555
},
4656
});
4757
}

packages/twenty-apps/public/call-recorder/src/front-components/utils/get-normalized-number-value.util.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Returns undefined when the draft cannot be stored, so a half-typed number is
22
// kept on screen instead of being persisted as a broken value.
3-
export const getNormalizedNumberValue = (
4-
value: string,
5-
): string | undefined => {
3+
export const getNormalizedNumberValue = (value: string): string | undefined => {
64
const trimmedValue = value.trim();
75

86
if (trimmedValue === '') {

packages/twenty-apps/public/call-recorder/src/front-components/utils/request-calendar-bot-scheduling-sync.util.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { SYNC_CALENDAR_BOT_SCHEDULING_ROUTE_PATH } from 'src/constants/sync-cale
55

66
type SyncCalendarBotSchedulingResponse = {
77
outcome?: string;
8-
canceledCallRecordingIds?: string[];
9-
failedCallRecordingIds?: string[];
8+
canceledCallRecordingCount?: number;
109
};
1110

1211
const buildSnackbarForResponse = (
@@ -16,15 +15,7 @@ const buildSnackbarForResponse = (
1615
return undefined;
1716
}
1817

19-
const canceledCount = (response.canceledCallRecordingIds ?? []).length;
20-
const failedCount = (response.failedCallRecordingIds ?? []).length;
21-
22-
if (failedCount > 0) {
23-
return {
24-
message: `Could not cancel ${failedCount} scheduled recording${failedCount === 1 ? '' : 's'}.`,
25-
variant: 'error',
26-
};
27-
}
18+
const canceledCount = response.canceledCallRecordingCount ?? 0;
2819

2920
if (canceledCount === 0) {
3021
return undefined;

packages/twenty-apps/public/call-recorder/src/logic-functions/__tests__/schedule-recall-bot-on-call-recording-update.test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ type HandlerEvent = Parameters<
3636
typeof scheduleRecallBotOnCallRecordingUpdateHandler
3737
>[0];
3838

39-
const buildUpdateEvent = (overrides: Partial<HandlerEvent> = {}): HandlerEvent =>
39+
const buildUpdateEvent = (
40+
overrides: Partial<HandlerEvent> = {},
41+
): HandlerEvent =>
4042
({
4143
name: 'callRecording.updated',
4244
recordId: 'call-recording-1',
@@ -150,9 +152,8 @@ describe('scheduleRecallBotOnCallRecordingUpdateHandler', () => {
150152
it('schedules a bot when an update clears the bot id of a requested recording', async () => {
151153
stubPendingCallRecordingQueries();
152154

153-
const result = await scheduleRecallBotOnCallRecordingUpdateHandler(
154-
buildUpdateEvent(),
155-
);
155+
const result =
156+
await scheduleRecallBotOnCallRecordingUpdateHandler(buildUpdateEvent());
156157

157158
expect(result).toEqual({
158159
callRecordingId: 'call-recording-1',
@@ -256,9 +257,8 @@ describe('scheduleRecallBotOnCallRecordingUpdateHandler', () => {
256257
]),
257258
}));
258259

259-
const result = await scheduleRecallBotOnCallRecordingUpdateHandler(
260-
buildUpdateEvent(),
261-
);
260+
const result =
261+
await scheduleRecallBotOnCallRecordingUpdateHandler(buildUpdateEvent());
262262

263263
expect(result).toEqual({
264264
callRecordingId: 'call-recording-1',
@@ -313,9 +313,8 @@ describe('scheduleRecallBotOnCallRecordingUpdateHandler', () => {
313313
]),
314314
}));
315315

316-
const result = await scheduleRecallBotOnCallRecordingUpdateHandler(
317-
buildUpdateEvent(),
318-
);
316+
const result =
317+
await scheduleRecallBotOnCallRecordingUpdateHandler(buildUpdateEvent());
319318

320319
expect(result).toEqual({
321320
callRecordingId: 'call-recording-1',

0 commit comments

Comments
 (0)