diff --git a/apps/web/lib/connector/hub-client.ts b/apps/web/lib/connector/hub-client.ts new file mode 100644 index 0000000000..70c008b5b7 --- /dev/null +++ b/apps/web/lib/connector/hub-client.ts @@ -0,0 +1,26 @@ +import "server-only"; + +import FormbricksHub from "@formbricks/hub"; +import { env } from "@/lib/env"; + +const globalForHub = globalThis as unknown as { + formbricksHubClient: FormbricksHub | undefined; +}; + +/** + * Returns a shared Formbricks Hub API client when HUB_API_KEY is set. + * Uses a global singleton so the same instance is reused across the process + * (and across Next.js HMR in development). When the key is not set, returns + * null and does not cache that result so a later call with the key set + * can create the client. + */ +export function getHubClient(): FormbricksHub | null { + if (globalForHub.formbricksHubClient) { + return globalForHub.formbricksHubClient; + } + const apiKey = env.HUB_API_KEY; + if (!apiKey) return null; + const client = new FormbricksHub({ apiKey, baseURL: env.HUB_API_URL }); + globalForHub.formbricksHubClient = client; + return client; +} diff --git a/apps/web/lib/connector/pipeline-handler.test.ts b/apps/web/lib/connector/pipeline-handler.test.ts index 4716d6aed3..c0f2998cae 100644 --- a/apps/web/lib/connector/pipeline-handler.test.ts +++ b/apps/web/lib/connector/pipeline-handler.test.ts @@ -125,7 +125,8 @@ describe("handleConnectorPipeline", () => { }); test("continues when transform returns no feedback records", async () => { - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); + const connector = createConnector(); + vi.mocked(getConnectorsBySurveyId).mockResolvedValue([connector]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue([]); await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); @@ -133,13 +134,27 @@ describe("handleConnectorPipeline", () => { expect(transformResponseToFeedbackRecords).toHaveBeenCalledWith( mockResponse, mockSurvey, - createConnector().formbricksMappings, + connector.formbricksMappings, "env-1" ); expect(mockFeedbackRecordsCreate).not.toHaveBeenCalled(); expect(updateConnector).not.toHaveBeenCalled(); }); + test("updates connector to error when HUB_API_KEY is not set", async () => { + vi.mocked(env).HUB_API_KEY = undefined as any; + vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); + vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); + + await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); + + expect(mockFeedbackRecordsCreate).not.toHaveBeenCalled(); + expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", { + status: "error", + errorMessage: expect.stringContaining("HUB_API_KEY"), + }); + }); + test("sends records to Hub and updates connector to active on full success", async () => { vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); @@ -156,20 +171,6 @@ describe("handleConnectorPipeline", () => { }); }); - test("updates connector to error when HUB_API_KEY is not set", async () => { - vi.mocked(env).HUB_API_KEY = undefined as any; - vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); - vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); - - await handleConnectorPipeline(mockResponse, mockSurvey, "env-1"); - - expect(mockFeedbackRecordsCreate).not.toHaveBeenCalled(); - expect(updateConnector).toHaveBeenCalledWith("conn-1", "env-1", { - status: "error", - errorMessage: expect.stringContaining("HUB_API_KEY"), - }); - }); - test("updates connector to error when all Hub creates fail", async () => { vi.mocked(getConnectorsBySurveyId).mockResolvedValue([createConnector()]); vi.mocked(transformResponseToFeedbackRecords).mockReturnValue(oneFeedbackRecord as any); diff --git a/apps/web/lib/connector/pipeline-handler.ts b/apps/web/lib/connector/pipeline-handler.ts index 1f2022ad58..be9d3322ac 100644 --- a/apps/web/lib/connector/pipeline-handler.ts +++ b/apps/web/lib/connector/pipeline-handler.ts @@ -1,27 +1,18 @@ import "server-only"; + import FormbricksHub from "@formbricks/hub"; import { logger } from "@formbricks/logger"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { env } from "@/lib/env"; import { getConnectorsBySurveyId, updateConnector } from "./service"; +import { getHubClient } from "./hub-client"; import { transformResponseToFeedbackRecords } from "./transform"; type FeedbackRecordCreateParams = FormbricksHub.FeedbackRecordCreateParams; -type FeedbackRecordData = FormbricksHub.FeedbackRecordData; - -function getHubClient(): FormbricksHub | null { - const apiKey = env.HUB_API_KEY; - if (!apiKey) return null; - return new FormbricksHub({ - apiKey, - baseURL: env.HUB_API_URL ?? undefined, - }); -} async function createFeedbackRecordsBatch(inputs: FeedbackRecordCreateParams[]): Promise<{ results: Array<{ - data: FeedbackRecordData | null; + data: FormbricksHub.FeedbackRecordData | null; error: { status: number; message: string; detail: string } | null; }>; }> { diff --git a/apps/web/package.json b/apps/web/package.json index b77498a462..45af514061 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,7 +30,7 @@ "@formbricks/cache": "workspace:*", "@formbricks/database": "workspace:*", "@formbricks/email": "workspace:*", - "@formbricks/hub": "^0.3.0", + "@formbricks/hub": "0.3.0", "@formbricks/i18n-utils": "workspace:*", "@formbricks/js-core": "workspace:*", "@formbricks/logger": "workspace:*",