Add call recorder oom diagnosis logs (#22315)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22315?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
nitin
2026-06-29 20:44:06 +05:30
committed by GitHub
parent 2de681da72
commit f263edd122
2 changed files with 159 additions and 8 deletions
@@ -106,6 +106,19 @@ const handleRecallStatusEvent = async ({
};
}
const shouldLogTerminalDiagnostics = isRecallRecordingDoneSignal({
event,
statusCode,
});
if (shouldLogTerminalDiagnostics) {
logRecallWebhookPhase({
phase: 'match-start',
webhookEvent,
callRecordingStatus,
});
}
const callRecording = await findMatchingCallRecording({
client,
webhookEvent,
@@ -144,12 +157,28 @@ const handleRecallStatusEvent = async ({
...buildRecordingTimestampsUpdate({ webhookEvent, callRecording }),
};
if (isRecallRecordingDoneSignal({ event, statusCode })) {
if (shouldLogTerminalDiagnostics) {
logRecallWebhookPhase({
phase: 'terminal-start',
webhookEvent,
callRecording,
callRecordingStatus,
});
const externalRecordingIdResolution = await resolveExternalRecordingId({
callRecording,
webhookEvent,
});
logRecallWebhookPhase({
phase: 'recording-id-resolved',
webhookEvent,
callRecording,
externalRecordingId: externalRecordingIdResolution.externalRecordingId,
providerLookupFailed: externalRecordingIdResolution.providerLookupFailed,
callRecordingStatus,
});
Object.assign(
updateData,
await buildTranscriptArtifactUpdate({
@@ -158,6 +187,15 @@ const handleRecallStatusEvent = async ({
}),
);
logRecallWebhookPhase({
phase: 'transcript-complete',
webhookEvent,
callRecording,
externalRecordingId: externalRecordingIdResolution.externalRecordingId,
updateData,
callRecordingStatus,
});
Object.assign(
updateData,
await buildMediaIngestionUpdate({
@@ -186,6 +224,18 @@ const handleRecallStatusEvent = async ({
updateData,
});
if (shouldLogTerminalDiagnostics) {
logRecallWebhookPhase({
phase: 'terminal-complete',
webhookEvent,
callRecording,
updateData,
callRecordingStatus: completesIngestion
? CallRecordingStatus.COMPLETED
: (updateData.status ?? callRecordingStatus),
});
}
return {
status: 'updated',
event,
@@ -196,6 +246,69 @@ const handleRecallStatusEvent = async ({
};
};
const logRecallWebhookPhase = ({
phase,
webhookEvent,
callRecording,
callRecordingStatus,
externalRecordingId,
providerLookupFailed,
updateData,
}: {
phase: string;
webhookEvent: RecallWebhookEvent;
callRecording?: MatchedCallRecording;
callRecordingStatus?: string;
externalRecordingId?: string;
providerLookupFailed?: boolean;
updateData?: CallRecordingUpdateFields;
}) => {
console.log(
[
`[call-recorder] recall-webhook phase=${phase}`,
`event=${webhookEvent.event}`,
`statusCode=${webhookEvent.statusCode ?? 'n/a'}`,
`callRecordingId=${callRecording?.id ?? webhookEvent.callRecordingIdFromMetadata ?? 'n/a'}`,
`externalBotId=${webhookEvent.externalBotId ?? 'n/a'}`,
`externalRecordingId=${externalRecordingId ?? webhookEvent.externalRecordingId ?? callRecording?.externalRecordingId ?? 'n/a'}`,
`callRecordingStatus=${callRecordingStatus ?? 'n/a'}`,
`currentStatus=${callRecording?.status ?? 'n/a'}`,
`hasTranscript=${hasReachableTranscript(callRecording?.transcript)}`,
`hasAudio=${isNonEmptyArray(callRecording?.audio)}`,
`hasVideo=${isNonEmptyArray(callRecording?.video)}`,
`updates=${formatUpdateDataKeys(updateData)}`,
`providerLookupFailed=${providerLookupFailed ?? false}`,
formatMemoryUsageForLog(),
].join(' '),
);
};
const formatUpdateDataKeys = (
updateData: CallRecordingUpdateFields | undefined,
): string => {
if (isUndefined(updateData)) {
return 'none';
}
const updateDataKeys = Object.keys(updateData);
return updateDataKeys.length === 0 ? 'none' : updateDataKeys.join(',');
};
const formatMemoryUsageForLog = (): string => {
const memoryUsage = process.memoryUsage();
return [
`rssMegaBytes=${formatBytesAsMegaBytes(memoryUsage.rss)}`,
`heapUsedMegaBytes=${formatBytesAsMegaBytes(memoryUsage.heapUsed)}`,
`externalMegaBytes=${formatBytesAsMegaBytes(memoryUsage.external)}`,
`arrayBuffersMegaBytes=${formatBytesAsMegaBytes(memoryUsage.arrayBuffers)}`,
].join(' ');
};
const formatBytesAsMegaBytes = (bytes: number): string =>
(bytes / 1024 / 1024).toFixed(1);
const findMatchingCallRecording = async ({
client,
webhookEvent,
@@ -91,7 +91,16 @@ const ingestMediaArtifact = async ({
fieldMetadataUniversalIdentifier: string;
}): Promise<CallRecordingMediaFile[] | undefined> => {
try {
const { buffer, contentType } = await downloadMediaFile(url);
const { buffer, contentType } = await downloadMediaFile({
callRecordingId,
fileName,
url,
});
console.log(
`[call-recorder] media-ingestion phase=artifact-upload-start callRecordingId=${callRecordingId} fileName=${fileName} downloadedBytes=${buffer.byteLength} contentType=${contentType} ${formatMemoryUsageForLog()}`,
);
const uploadedFile = await metadataClient.uploadFile(
buffer,
fileName,
@@ -109,20 +118,49 @@ const ingestMediaArtifact = async ({
}
};
const downloadMediaFile = async (
url: string,
): Promise<{ buffer: Buffer; contentType: string }> => {
const downloadMediaFile = async ({
callRecordingId,
fileName,
url,
}: {
callRecordingId: string;
fileName: string;
url: string;
}): Promise<{ buffer: Buffer; contentType: string }> => {
const response = await fetch(url, {
signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS),
});
const contentType =
response.headers.get('content-type') ?? 'application/octet-stream';
const contentLength = response.headers.get('content-length') ?? 'unknown';
console.log(
`[call-recorder] media-ingestion phase=artifact-download-response callRecordingId=${callRecordingId} fileName=${fileName} responseStatus=${response.status} contentLengthBytes=${contentLength} contentType=${contentType} ${formatMemoryUsageForLog()}`,
);
if (!response.ok) {
throw new Error(`download failed with status ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return {
buffer: Buffer.from(await response.arrayBuffer()),
contentType:
response.headers.get('content-type') ?? 'application/octet-stream',
buffer,
contentType,
};
};
const formatMemoryUsageForLog = (): string => {
const memoryUsage = process.memoryUsage();
return [
`rssMegaBytes=${formatBytesAsMegaBytes(memoryUsage.rss)}`,
`heapUsedMegaBytes=${formatBytesAsMegaBytes(memoryUsage.heapUsed)}`,
`externalMegaBytes=${formatBytesAsMegaBytes(memoryUsage.external)}`,
`arrayBuffersMegaBytes=${formatBytesAsMegaBytes(memoryUsage.arrayBuffers)}`,
].join(' ');
};
const formatBytesAsMegaBytes = (bytes: number): string =>
(bytes / 1024 / 1024).toFixed(1);