From f8ed432e2aa2c7df4031a9ddfde88c8a60d25de7 Mon Sep 17 00:00:00 2001
From: nitin <142569587+ehconitin@users.noreply.github.com>
Date: Fri, 31 Jul 2026 16:56:46 +0530
Subject: [PATCH] Harden Fireflies call synchronization lifecycle (#23610)
Makes Fireflies call syncing (webhook and manual) resilient and
lifecycle-correct.
- Keeps a call recording `PROCESSING` until both transcript and summary
are filled, then marks it `COMPLETED`
- Looks up existing call recording field state first so only missing
fields are fetched from Fireflies
- Makes the call recording write race-safe: deterministic-id create with
a concurrent-create fallback
- Adds bounded retries with rate-limit handling to Fireflies API
requests
- Bumps twenty-sdk to 2.25.0 and validates query results with zod
---
.../twenty-apps/public/fireflies/package.json | 3 +-
.../twenty-fireflies.integration-test.ts | 11 +-
.../call-recording-request-status.constant.ts | 3 +
...s.ts => call-recording-status.constant.ts} | 4 -
.../fireflies-api-max-attempts.constant.ts | 1 +
...s-api-retry-delay-milliseconds.constant.ts | 1 +
...ll-recording-field-states-or-throw.util.ts | 52 ++++
...rt-missing-fireflies-calls.test-support.ts | 173 +++++++++++
.../fireflies-webhook-handler.test.ts | 77 +++++
.../handlers/fireflies-sync-call-handler.ts | 48 +++-
.../handlers/fireflies-webhook-handler.ts | 17 +-
...ng-field-state-query-result.schema.test.ts | 33 +++
...cording-field-state-query-result.schema.ts | 36 +++
.../types/call-recording-field-state.type.ts | 9 +
.../types/call-recording-sync-fields.type.ts | 17 ++
.../types/fireflies-sync-call-result.type.ts | 2 +-
.../types/fireflies-syncable-field.type.ts | 1 +
.../types/sync-fireflies-call-result.type.ts | 17 ++
.../types/transcript-entry-word.type.ts | 5 +
.../types/transcript-entry.type.ts | 6 +
.../__tests__/fireflies-api-request.test.ts | 103 +++++++
...es-sentences-to-transcript-entries.test.ts | 2 +-
...c-fireflies-call-to-call-recording.test.ts | 149 ++++++++++
.../utils/fireflies-api-request.ts | 54 +++-
.../is-retryable-fireflies-api-status.util.ts | 3 +
...s-sentences-to-transcript-entries.util.ts} | 12 +-
.../sync-fireflies-call-to-call-recording.ts | 186 ------------
...c-fireflies-call-to-call-recording.util.ts | 268 ++++++++++++++++++
.../upsert-call-recording-or-throw.util.ts | 108 +++++++
.../utils/upsert-call-recording.ts | 96 -------
.../src/utils/sleep-for-milliseconds.util.ts | 2 +
.../twenty-apps/public/fireflies/yarn.lock | 1 +
32 files changed, 1177 insertions(+), 323 deletions(-)
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-request-status.constant.ts
rename packages/twenty-apps/public/fireflies/src/logic-functions/constants/{call-recording-status.ts => call-recording-status.constant.ts} (70%)
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-max-attempts.constant.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-retry-delay-milliseconds.constant.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/data/find-call-recording-field-states-or-throw.util.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/flows/__tests__/import-missing-fireflies-calls.test-support.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/handlers/__tests__/fireflies-webhook-handler.test.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/schemas/__tests__/call-recording-field-state-query-result.schema.test.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/schemas/call-recording-field-state-query-result.schema.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/types/call-recording-field-state.type.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/types/call-recording-sync-fields.type.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/types/fireflies-syncable-field.type.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/types/sync-fireflies-call-result.type.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/types/transcript-entry-word.type.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/types/transcript-entry.type.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/__tests__/fireflies-api-request.test.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/__tests__/sync-fireflies-call-to-call-recording.test.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/is-retryable-fireflies-api-status.util.ts
rename packages/twenty-apps/public/fireflies/src/logic-functions/utils/{map-fireflies-sentences-to-transcript-entries.ts => map-fireflies-sentences-to-transcript-entries.util.ts} (86%)
delete mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/sync-fireflies-call-to-call-recording.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/sync-fireflies-call-to-call-recording.util.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/upsert-call-recording-or-throw.util.ts
delete mode 100644 packages/twenty-apps/public/fireflies/src/logic-functions/utils/upsert-call-recording.ts
create mode 100644 packages/twenty-apps/public/fireflies/src/utils/sleep-for-milliseconds.util.ts
diff --git a/packages/twenty-apps/public/fireflies/package.json b/packages/twenty-apps/public/fireflies/package.json
index 21f91d0356..8d20b08d11 100644
--- a/packages/twenty-apps/public/fireflies/package.json
+++ b/packages/twenty-apps/public/fireflies/package.json
@@ -23,7 +23,8 @@
"test:unit": "vitest run --config vitest.unit.config.ts"
},
"dependencies": {
- "@sniptt/guards": "^0.2.0"
+ "@sniptt/guards": "^0.2.0",
+ "zod": "^4.1.11"
},
"devDependencies": {
"@emotion/react": "^11.14.0",
diff --git a/packages/twenty-apps/public/fireflies/src/__tests__/twenty-fireflies.integration-test.ts b/packages/twenty-apps/public/fireflies/src/__tests__/twenty-fireflies.integration-test.ts
index 4d0af6b506..9996562fac 100644
--- a/packages/twenty-apps/public/fireflies/src/__tests__/twenty-fireflies.integration-test.ts
+++ b/packages/twenty-apps/public/fireflies/src/__tests__/twenty-fireflies.integration-test.ts
@@ -3,10 +3,8 @@ import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { describe, expect, it } from 'vitest';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
-import {
- CALL_RECORDING_REQUEST_STATUS,
- CALL_RECORDING_STATUS,
-} from 'src/logic-functions/constants/call-recording-status';
+import { CALL_RECORDING_REQUEST_STATUS } from 'src/logic-functions/constants/call-recording-request-status.constant';
+import { CALL_RECORDING_STATUS } from 'src/logic-functions/constants/call-recording-status.constant';
describe('App installation', () => {
it('should find the installed Fireflies app in the applications list', async () => {
@@ -37,9 +35,8 @@ type GeneratedCoreSchemaRuntime = {
};
const getServerCallRecordingStatuses = async (): Promise => {
- const generatedCoreSchema = (await import(
- 'twenty-client-sdk/core'
- )) as unknown as GeneratedCoreSchemaRuntime;
+ const generatedCoreSchema =
+ (await import('twenty-client-sdk/core')) as unknown as GeneratedCoreSchemaRuntime;
const statusEnum = generatedCoreSchema.enumCallRecordingStatusEnum;
if (statusEnum === undefined) {
diff --git a/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-request-status.constant.ts b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-request-status.constant.ts
new file mode 100644
index 0000000000..db4a7e1caf
--- /dev/null
+++ b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-request-status.constant.ts
@@ -0,0 +1,3 @@
+export const CALL_RECORDING_REQUEST_STATUS = {
+ REQUESTED: 'REQUESTED',
+} as const;
diff --git a/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-status.ts b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-status.constant.ts
similarity index 70%
rename from packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-status.ts
rename to packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-status.constant.ts
index 3f242047a9..d56bfc4f8a 100644
--- a/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-status.ts
+++ b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/call-recording-status.constant.ts
@@ -3,7 +3,3 @@ export const CALL_RECORDING_STATUS = {
PROCESSING: 'PROCESSING',
COMPLETED: 'COMPLETED',
} as const;
-
-export const CALL_RECORDING_REQUEST_STATUS = {
- REQUESTED: 'REQUESTED',
-} as const;
diff --git a/packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-max-attempts.constant.ts b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-max-attempts.constant.ts
new file mode 100644
index 0000000000..93566b3c39
--- /dev/null
+++ b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-max-attempts.constant.ts
@@ -0,0 +1 @@
+export const FIREFLIES_API_MAX_ATTEMPTS = 3;
diff --git a/packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-retry-delay-milliseconds.constant.ts b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-retry-delay-milliseconds.constant.ts
new file mode 100644
index 0000000000..54233f20fb
--- /dev/null
+++ b/packages/twenty-apps/public/fireflies/src/logic-functions/constants/fireflies-api-retry-delay-milliseconds.constant.ts
@@ -0,0 +1 @@
+export const FIREFLIES_API_RETRY_DELAY_MILLISECONDS = 250;
diff --git a/packages/twenty-apps/public/fireflies/src/logic-functions/data/find-call-recording-field-states-or-throw.util.ts b/packages/twenty-apps/public/fireflies/src/logic-functions/data/find-call-recording-field-states-or-throw.util.ts
new file mode 100644
index 0000000000..72cc7bb762
--- /dev/null
+++ b/packages/twenty-apps/public/fireflies/src/logic-functions/data/find-call-recording-field-states-or-throw.util.ts
@@ -0,0 +1,52 @@
+import { isNonEmptyString } from '@sniptt/guards';
+import { type CoreApiClient } from 'twenty-client-sdk/core';
+import { isDefined } from 'src/utils/is-defined';
+
+import { callRecordingFieldStateQueryResultSchema } from 'src/logic-functions/schemas/call-recording-field-state-query-result.schema';
+import { type CallRecordingFieldState } from 'src/logic-functions/types/call-recording-field-state.type';
+
+export const findCallRecordingFieldStatesOrThrow = async ({
+ coreApiClient,
+ callRecordingIds,
+}: {
+ coreApiClient: CoreApiClient;
+ callRecordingIds: string[];
+}): Promise