From 731f97ebe4c0466569acfe5f861075afe6f6fe75 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Sat, 19 Oct 2024 19:52:04 +0530 Subject: [PATCH] fix: Add troubleshooting data and ensure no errors in live mode of router. (#17188) --- apps/web/components/dialog/RerouteDialog.tsx | 7 +- .../dialog/__tests__/RerouteDialog.test.tsx | 16 +- .../routing-forms/components/SingleForm.tsx | 15 +- .../config/config.tsx | 2 +- .../trpc/__tests__/utils.test.ts | 292 ++++++++++++++++-- ...amMembersMatchingAttributeLogic.handler.ts | 44 ++- ...eamMembersMatchingAttributeLogic.schema.ts | 1 + .../app-store/routing-forms/trpc/utils.ts | 141 +++++++-- 8 files changed, 432 insertions(+), 86 deletions(-) diff --git a/apps/web/components/dialog/RerouteDialog.tsx b/apps/web/components/dialog/RerouteDialog.tsx index afec6be8aa..fe04c10004 100644 --- a/apps/web/components/dialog/RerouteDialog.tsx +++ b/apps/web/components/dialog/RerouteDialog.tsx @@ -27,7 +27,6 @@ import type { Ensure } from "@calcom/types/utils"; import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui"; import { Button, Tooltip } from "@calcom/ui"; import { showToast } from "@calcom/ui/components/toast"; -import useRouterQuery from "@calcom/web/lib/hooks/useRouterQuery"; const enum ReroutingStatusEnum { REROUTING_NOT_INITIATED = "not_initiated", @@ -599,9 +598,9 @@ const NewRoutingManager = ({ } function reroutingCTAs() { - if(chosenRoute.action.type !== RouteActionType.EventTypeRedirectUrl) { + if (chosenRoute.action.type !== RouteActionType.EventTypeRedirectUrl) { // There are no actions to perform if the new chosen route isn't an event type redirect - return null + return null; } const shouldDisableCTAs = teamMembersMatchingAttributeLogic.isPending || createBookingMutation.isPending; return ( @@ -766,7 +765,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ const findTeamMembersMatchingAttributeLogicMutation = trpc.viewer.appRoutingForms.findTeamMembersMatchingAttributeLogic.useMutation({ onSuccess(data) { - setTeamMembersMatchingAttributeLogic(data); + setTeamMembersMatchingAttributeLogic(data.result); }, }); diff --git a/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx b/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx index 0cebf9ecee..13dc8af70d 100644 --- a/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx +++ b/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx @@ -168,13 +168,15 @@ vi.mock("@calcom/trpc/react", () => ({ useMutation: vi.fn(({ onSuccess }) => { return { mutate: vi.fn(() => { - onSuccess([ - { - id: 1, - name: "Matching User 1", - email: "matching-user-1@example.com", - }, - ]); + onSuccess({ + result: [ + { + id: 1, + name: "Matching User 1", + email: "matching-user-1@example.com", + }, + ], + }); }), }; }), diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 8fd876f1cd..aa6454e568 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -8,8 +8,9 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequir import AddMembersWithSwitch from "@calcom/features/eventtypes/components/AddMembersWithSwitch"; import { ShellMain } from "@calcom/features/shell/Shell"; import useApp from "@calcom/lib/hooks/useApp"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc/react"; +import { trpc, TRPCClientError } from "@calcom/trpc/react"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Alert, @@ -242,6 +243,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF const [chosenRoute, setChosenRoute] = useState(null); const [skipFirstUpdate, setSkipFirstUpdate] = useState(true); const [eventTypeUrl, setEventTypeUrl] = useState(""); + const searchParams = useCompatSearchParams(); const [teamMembersMatchingAttributeLogic, setTeamMembersMatchingAttributeLogic] = useState< | { id: number; @@ -253,7 +255,14 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF const findTeamMembersMatchingAttributeLogicMutation = trpc.viewer.appRoutingForms.findTeamMembersMatchingAttributeLogic.useMutation({ onSuccess(data) { - setTeamMembersMatchingAttributeLogic(data); + setTeamMembersMatchingAttributeLogic(data.result); + }, + onError(e) { + if (e instanceof TRPCClientError) { + showToast(e.message, "error"); + } else { + showToast(t("something_went_wrong"), "error"); + } }, }); @@ -280,6 +289,8 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF formId: form.id, response, routeId: route.id, + isPreview: true, + _enablePerf: searchParams.get("enablePerf") === "true", }); } diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/config.tsx b/packages/app-store/routing-forms/components/react-awesome-query-builder/config/config.tsx index f731978b36..c826768aa2 100644 --- a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/config.tsx +++ b/packages/app-store/routing-forms/components/react-awesome-query-builder/config/config.tsx @@ -54,7 +54,7 @@ function getSettings(_configFor: ConfigFor) { groupActionsPosition: "bottomCenter", // TODO: Test it and then enable it. It might allow us to show better error messages. // But it doesn't detect every kind of error like an operator gone missing e.g. what happened in https://github.com/calcom/cal.com/pull/17102 - // showErrorMessage: true, + showErrorMessage: true, // Disable groups maxNesting: 1, }; diff --git a/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts b/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts index f478b7082b..b4d7d6c122 100644 --- a/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts +++ b/packages/app-store/routing-forms/trpc/__tests__/utils.test.ts @@ -1,8 +1,8 @@ import type { BaseWidget } from "react-awesome-query-builder"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { AttributeType } from "@calcom/prisma/enums"; import { RouteActionType } from "@calcom/app-store/routing-forms/zod"; +import type { AttributeType } from "@calcom/prisma/enums"; import { RoutingFormFieldType } from "../../lib/FieldTypes"; import { RaqbLogicResult } from "../../lib/evaluateRaqbLogic"; @@ -176,15 +176,17 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { vi.resetAllMocks(); }); - it("should return null if route is not found", async () => { - const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({ - form: { routes: [], fields: [] }, - response: {}, - routeId: "non-existent-route", - teamId: 1, - }); + it("should return null if route is not found and troubleshooter should also be null by default", async () => { + const { teamMembersMatchingAttributeLogic: result, troubleshooter } = + await findTeamMembersMatchingAttributeLogicOfRoute({ + form: { routes: [], fields: [] }, + response: {}, + routeId: "non-existent-route", + teamId: 1, + }); expect(result).toBeNull(); + expect(troubleshooter).toBeNull(); }); it("should return null if the route does not have an attributesQueryValue set", async () => { @@ -207,7 +209,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { expect(result).toBeNull(); }); - it("should return matching team members with a SINGLE_SELECT attribute when a static option is selected", async () => { + it("should return matching team members with a SINGLE_SELECT attribute when a static option is selected and troubleshooter should be null by default", async () => { const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" }; const Attribute1 = { id: "attr1", @@ -234,24 +236,25 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { ], }) as AttributesQueryValue; - const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({ - form: { - routes: [ - { - id: "test-route", - action: { type: RouteActionType.CustomPageMessage, value: "test" }, - queryValue: { - type: "group", - } as unknown as FormFieldsQueryValue, - attributesQueryValue: attributesQueryValue, - }, - ], - fields: [], - }, - response: {}, - routeId: "test-route", - teamId: 1, - }); + const { teamMembersMatchingAttributeLogic: result, troubleshooter } = + await findTeamMembersMatchingAttributeLogicOfRoute({ + form: { + routes: [ + { + id: "test-route", + action: { type: RouteActionType.CustomPageMessage, value: "test" }, + queryValue: { + type: "group", + } as unknown as FormFieldsQueryValue, + attributesQueryValue: attributesQueryValue, + }, + ], + fields: [], + }, + response: {}, + routeId: "test-route", + teamId: 1, + }); expect(result).toEqual([ { @@ -259,6 +262,8 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { result: RaqbLogicResult.MATCH, }, ]); + + expect(troubleshooter).toBeNull(); }); it("should return matching team members with a SINGLE_SELECT attribute when 'Value of Field' option is selected", async () => { @@ -339,8 +344,6 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { it("should return matching team members with a SINGLE_SELECT attribute when 'Any in' option is selected", async () => { const Option1OfAttribute1HumanReadableValue = "Option 1"; - const Option1OfField1HumanReadableValue = Option1OfAttribute1HumanReadableValue; - const Field1Id = "field-1"; const Option1OfAttribute1 = { id: "attr-1-opt-1", @@ -454,6 +457,158 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { }) ).rejects.toThrow("Unsupported attribute type"); }); + + it("should not throw error in live (non-preview) mode but should throw in preview mode", async () => { + const Option1OfAttribute1HumanReadableValue = "Option 1"; + + const Option1OfAttribute1 = { + id: "attr-1-opt-1", + value: Option1OfAttribute1HumanReadableValue, + slug: "option-1", + }; + + const Option2OfAttribute1 = { + id: "attr-1-opt-2", + value: "Option 2", + slug: "option-2", + }; + + const Option3OfAttribute1 = { + id: "attr-1-opt-3", + value: "Option 3", + slug: "option-3", + }; + + const Attribute1 = { + id: "attr1", + name: "Attribute 1", + type: "SINGLE_SELECT" as const, + slug: "attribute-1", + options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1], + }; + + mockAttributesScenario({ + attributes: [Attribute1], + teamMembersWithAttributeOptionValuePerAttribute: [ + { userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } }, + ], + }); + + const attributesQueryValue = buildSelectTypeFieldQueryValue({ + rules: [ + { + raqbFieldId: Attribute1.id, + value: [["NON_EXISTING_OPTION_1", "NON_EXISTING_OPTION_2"]], + operator: "select_any_in", + valueType: ["multiselect"], + }, + ], + }) as AttributesQueryValue; + + async function runInMode({ mode }: { mode: "preview" | "live" }) { + const { teamMembersMatchingAttributeLogic: result } = + await findTeamMembersMatchingAttributeLogicOfRoute({ + form: { + routes: [ + buildDefaultCustomPageRoute({ + id: "test-route", + attributesQueryValue: attributesQueryValue, + }), + ], + fields: [], + }, + response: {}, + routeId: "test-route", + teamId: 1, + isPreview: mode === "preview" ? true : false, + }); + return result; + } + + await (async function liveMode() { + const result = await runInMode({ mode: "live" }); + expect(result).toEqual([]); + })(); + + await (async function previewMode() { + expect(() => runInMode({ mode: "preview" })).rejects.toThrow( + /Value NON_EXISTING_OPTION_1 is not in list of values/ + ); + })(); + }); + + it("should not throw error if children1 is empty", async () => { + const Option1OfAttribute1HumanReadableValue = "Option 1"; + + const Option1OfAttribute1 = { + id: "attr-1-opt-1", + value: Option1OfAttribute1HumanReadableValue, + slug: "option-1", + }; + + const Option2OfAttribute1 = { + id: "attr-1-opt-2", + value: "Option 2", + slug: "option-2", + }; + + const Option3OfAttribute1 = { + id: "attr-1-opt-3", + value: "Option 3", + slug: "option-3", + }; + + const Attribute1 = { + id: "attr1", + name: "Attribute 1", + type: "SINGLE_SELECT" as const, + slug: "attribute-1", + options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1], + }; + + mockAttributesScenario({ + attributes: [Attribute1], + teamMembersWithAttributeOptionValuePerAttribute: [ + { userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } }, + ], + }); + + const attributesQueryValue = buildQueryValue({ + rules: [], + }) as AttributesQueryValue; + + expect(attributesQueryValue.children1).toEqual({}); + + async function runInMode({ mode }: { mode: "preview" | "live" }) { + const { teamMembersMatchingAttributeLogic: result } = + await findTeamMembersMatchingAttributeLogicOfRoute({ + form: { + routes: [ + buildDefaultCustomPageRoute({ + id: "test-route", + attributesQueryValue: attributesQueryValue, + }), + ], + fields: [], + }, + response: {}, + routeId: "test-route", + teamId: 1, + isPreview: mode === "preview" ? true : false, + }); + return result; + } + + await (async function liveMode() { + const result = await runInMode({ mode: "live" }); + expect(result).toEqual(null); + })(); + + await (async function previewMode() { + const result = await runInMode({ mode: "preview" }); + expect(result).toEqual(null); + })(); + }); }); describe("Performance testing", () => { @@ -537,4 +692,83 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => { }, 10000); }); }); + + describe("Troubleshooter", () => { + it("troubleshooter data should be there when enableTroubleshooter is true", async () => { + const Option1OfAttribute1HumanReadableValue = "Option 1"; + + const Option1OfAttribute1 = { + id: "attr-1-opt-1", + value: Option1OfAttribute1HumanReadableValue, + slug: "option-1", + }; + + const Option2OfAttribute1 = { + id: "attr-1-opt-2", + value: "Option 2", + slug: "option-2", + }; + + const Option3OfAttribute1 = { + id: "attr-1-opt-3", + value: "Option 3", + slug: "option-3", + }; + + const Attribute1 = { + id: "attr1", + name: "Attribute 1", + type: "SINGLE_SELECT" as const, + slug: "attribute-1", + options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1], + }; + + mockAttributesScenario({ + attributes: [Attribute1], + teamMembersWithAttributeOptionValuePerAttribute: [ + { userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } }, + ], + }); + + const attributesQueryValue = buildSelectTypeFieldQueryValue({ + rules: [ + { + raqbFieldId: Attribute1.id, + value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]], + operator: "select_any_in", + valueType: ["multiselect"], + }, + ], + }) as AttributesQueryValue; + + const { teamMembersMatchingAttributeLogic: result, troubleshooter } = + await findTeamMembersMatchingAttributeLogicOfRoute( + { + form: { + routes: [ + buildDefaultCustomPageRoute({ + id: "test-route", + attributesQueryValue: attributesQueryValue, + }), + ], + fields: [], + }, + response: {}, + routeId: "test-route", + teamId: 1, + }, + { + enableTroubleshooter: true, + } + ); + + expect(result).toEqual([ + { + userId: 1, + result: RaqbLogicResult.MATCH, + }, + ]); + expect(troubleshooter).not.toBeNull(); + }); + }); }); diff --git a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts index f2508741a5..18933e4615 100644 --- a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts +++ b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.handler.ts @@ -25,7 +25,7 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({ input, }: FindTeamMembersMatchingAttributeLogicHandlerOptions) => { const { prisma, user } = ctx; - const { formId, response, routeId, _enablePerf, _concurrency } = input; + const { formId, response, routeId, isPreview, _enablePerf, _concurrency } = input; const form = await prisma.app_RoutingForms_Form.findFirst({ where: { @@ -53,37 +53,47 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({ const { teamMembersMatchingAttributeLogic: matchingTeamMembersWithResult, timeTaken: teamMembersMatchingAttributeLogicTimeTaken, + troubleshooter, } = await findTeamMembersMatchingAttributeLogicOfRoute( { response, routeId, form: serializableForm, teamId: form.teamId, + isPreview: !!isPreview, }, { enablePerf: _enablePerf, + // Reuse same flag for enabling troubleshooter. We would normall use them together + enableTroubleshooter: _enablePerf, concurrency: _concurrency, } ); if (!matchingTeamMembersWithResult) { - return null; + return { + troubleshooter, + result: null, + }; } const matchingTeamMembersIds = matchingTeamMembersWithResult.map((member) => member.userId); const matchingTeamMembers = await UserRepository.findByIds({ ids: matchingTeamMembersIds }); - console.log('_enablePerf, _concurrency', _enablePerf, _concurrency) + console.log("_enablePerf, _concurrency", _enablePerf, _concurrency); if (_enablePerf) { const serverTimingHeader = getServerTimingHeader(teamMembersMatchingAttributeLogicTimeTaken); ctx.res?.setHeader("Server-Timing", serverTimingHeader); - console.log('Server-Timing', serverTimingHeader) + console.log("Server-Timing", serverTimingHeader); } - return matchingTeamMembers.map((user) => ({ - id: user.id, - name: user.name, - email: user.email, - })); + return { + troubleshooter, + result: matchingTeamMembers.map((user) => ({ + id: user.id, + name: user.name, + email: user.email, + })), + }; }; function getServerTimingHeader(timeTaken: { @@ -93,14 +103,16 @@ function getServerTimingHeader(timeTaken: { lgcFrMbrs: number | null; gQryVal: number | null; }) { - const headerParts = Object.entries(timeTaken).map(([key, value]) => { - if (value !== null) { - return `${key};dur=${value}`; - } - return null; - }).filter(Boolean); + const headerParts = Object.entries(timeTaken) + .map(([key, value]) => { + if (value !== null) { + return `${key};dur=${value}`; + } + return null; + }) + .filter(Boolean); - return headerParts.join(', '); + return headerParts.join(", "); } export default findTeamMembersMatchingAttributeLogicHandler; diff --git a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts index 181ff69b5f..8e34f55ddb 100644 --- a/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts +++ b/packages/app-store/routing-forms/trpc/findTeamMembersMatchingAttributeLogic.schema.ts @@ -4,6 +4,7 @@ export const ZFindTeamMembersMatchingAttributeLogicInputSchema = z.object({ formId: z.string(), response: z.record(z.string(), z.any()), routeId: z.string(), + isPreview: z.boolean().optional(), _enablePerf: z.boolean().optional(), _concurrency: z.number().optional(), }); diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index 3a5392404f..21a48c1be5 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -1,12 +1,13 @@ import type { App_RoutingForms_Form, User } from "@prisma/client"; import async from "async"; -import { Config, Utils as QbUtils } from "react-awesome-query-builder"; +import type { ImmutableTree, JsonTree } from "react-awesome-query-builder"; +import type { Config } from "react-awesome-query-builder/lib"; +import { Utils as QbUtils } from "react-awesome-query-builder/lib"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; import { WebhookTriggerEvents } from "@calcom/prisma/client"; import type { Ensure } from "@calcom/types/utils"; @@ -17,7 +18,7 @@ import { } from "../lib/getAttributes"; import isRouter from "../lib/isRouter"; import jsonLogic from "../lib/jsonLogic"; -import type { SerializableField, OrderedResponses, AttributesQueryValue } from "../types/types"; +import type { SerializableField, OrderedResponses } from "../types/types"; import type { FormResponse, SerializableForm } from "../types/types"; import { acrossQueryValueCompatiblity, raqbQueryValueUtils } from "./raqbUtils"; @@ -118,12 +119,38 @@ function perf(fn: () => ReturnValue): [ReturnValue, number | null] return [result, end - start]; } +function getErrorsFromImmutableTree(tree: ImmutableTree) { + const validatedQueryValue = QbUtils.getTree(tree); + if (!raqbQueryValueUtils.isQueryValueARuleGroup(validatedQueryValue)) { + return []; + } + + if (!validatedQueryValue.children1) { + return []; + } + + const errors: string[][] = []; + Object.values(validatedQueryValue.children1).map((rule) => { + if (rule.type !== "rule") { + return; + } + const valueError = rule.properties.valueError; + if (valueError) { + // Sometimes there are null values in it. + errors.push(valueError.filter((value) => !!value)); + } + }); + return errors; +} + function getJsonLogic({ attributesQueryValue, attributesQueryBuilderConfig, + isPreview, }: { - attributesQueryValue: AttributesQueryValue; + attributesQueryValue: JsonTree; attributesQueryBuilderConfig: Config; + isPreview: boolean; }) { const state = { tree: QbUtils.checkTree( @@ -136,45 +163,71 @@ function getJsonLogic({ const jsonLogicQuery = QbUtils.jsonLogicFormat(state.tree, state.config); const logic = jsonLogicQuery.logic; - if (!logic) { - if (attributesQueryValue.children1 && Object.keys(attributesQueryValue.children1).length > 0) { - throw new Error("Couldn't build the logic from the query value"); + + // We error only in preview mode to communicate any problem. + // In live mode, we don't error and instead prefer to let no members match which then causes all of the assignes of the team event to be used. + if (isPreview) { + const errors = getErrorsFromImmutableTree(state.tree).flat(); + if (errors.length) { + throw new Error(errors.toString()); + } + if (!logic) { + // Empty children1 is normal where it means that no rules are added by user. + if (attributesQueryValue.children1 && Object.keys(attributesQueryValue.children1).length > 0) { + // Possible reasons for this + // 1. The attribute option value used is not in the options list. Happens if 'Value of field' value is chosen and that field's response value doesn't exist in attribute options list. + throw new Error("There is some error building the logic, please check the routes."); + } } - console.log( - "No logic found", - safeStringify({ attributesQueryValue, queryBuilderConfigFields: attributesQueryBuilderConfig.fields }) - ); } - console.log("Using LOGIC", safeStringify(logic)); + return logic; } +export const enum TroubleshooterCase { + EMPTY_QUERY_VALUE = "empty-query-value", + IS_A_ROUTER = "is-a-router", + NO_LOGIC_FOUND = "no-logic-found", + MATCH_RESULTS_READY = "match-results-ready", + NO_ROUTE_FOUND = "no-route-found", +} + export async function findTeamMembersMatchingAttributeLogicOfRoute( { form, response, routeId, teamId, + isPreview, }: { form: Pick, "routes" | "fields">; response: FormResponse; routeId: string; teamId: number; + isPreview?: boolean; }, config: { enablePerf?: boolean; concurrency?: number; + enableTroubleshooter?: boolean; } = {} ) { const route = form.routes?.find((route) => route.id === routeId); - // Higher value of concurrency might not be performant as it might overwhelm the system. So, use a lower value as default. - const { enablePerf = false, concurrency = 2 } = config; + const { enablePerf = false, concurrency = 2, enableTroubleshooter } = config; if (!route) { return { teamMembersMatchingAttributeLogic: null, timeTaken: null, + troubleshooter: enableTroubleshooter + ? { + type: TroubleshooterCase.NO_ROUTE_FOUND, + data: { + routeId, + }, + } + : null, }; } @@ -182,6 +235,14 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute( return { teamMembersMatchingAttributeLogic: null, timeTaken: null, + troubleshooter: enableTroubleshooter + ? { + type: TroubleshooterCase.IS_A_ROUTER, + data: { + routeId, + }, + } + : null, }; } @@ -211,6 +272,14 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute( gMbrWtAtr: null, lgcFrMbrs: null, }, + troubleshooter: enableTroubleshooter + ? { + type: TroubleshooterCase.EMPTY_QUERY_VALUE, + data: { + attributesQueryValue, + }, + } + : null, }; } @@ -222,30 +291,20 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute( }) ); - moduleLogger.debug( - "Finding team members matching attribute logic", - safeStringify({ - form, - response, - routeId, - teamId, - attributesQueryBuilderConfigFields: attributesQueryBuilderConfig.fields, - }) - ); - const [ teamMembersWithAttributeOptionValuePerAttribute, getTeamMembersWithAttributeOptionValuePerAttributeTimeTaken, ] = await aPf(() => getTeamMembersWithAttributeOptionValuePerAttribute({ teamId: teamId })); const logic = getJsonLogic({ - attributesQueryValue, + attributesQueryValue: attributesQueryValue as JsonTree, attributesQueryBuilderConfig: attributesQueryBuilderConfig as unknown as Config, + isPreview: !!isPreview, }); if (!logic) { return { - teamMembersMatchingAttributeLogic: [], + teamMembersMatchingAttributeLogic: null, timeTaken: { gAtr: getAttributesForTeamTimeTaken, gQryCnfg: getAttributesQueryBuilderConfigTimeTaken, @@ -253,9 +312,21 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute( lgcFrMbrs: null, gQryVal: getAttributesQueryValueTimeTaken, }, + troubleshooter: enableTroubleshooter + ? { + type: TroubleshooterCase.NO_LOGIC_FOUND, + data: { + attributesQueryValue, + attributesQueryBuilderConfig, + teamMembersWithAttributeOptionValuePerAttribute, + }, + } + : null, }; } + const attributesDataPerUser = new Map>(); + const [_, teamMembersMatchingAttributeLogicTimeTaken] = await aPf(async () => { return await async.mapLimit>( teamMembersWithAttributeOptionValuePerAttribute, @@ -266,6 +337,10 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute( attributesQueryValue, }); + if (enableTroubleshooter) { + attributesDataPerUser.set(member.userId, attributesData); + } + const result = !!jsonLogic.apply(logic as any, attributesData) ? RaqbLogicResult.MATCH : RaqbLogicResult.NO_MATCH; @@ -290,6 +365,18 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute( lgcFrMbrs: teamMembersMatchingAttributeLogicTimeTaken, gQryVal: getAttributesQueryValueTimeTaken, }, + troubleshooter: enableTroubleshooter + ? { + type: TroubleshooterCase.MATCH_RESULTS_READY, + data: { + attributesDataPerUser, + attributesQueryValue, + attributesQueryBuilderConfig, + logic, + attributesForTeam, + }, + } + : null, }; function pf(fn: () => ReturnValue): [ReturnValue, number | null] {