fix: Add troubleshooting data and ensure no errors in live mode of router. (#17188)
This commit is contained in:
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -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<NonRouterRoute | null>(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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+28
-16
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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<ReturnValue>(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<SerializableForm<App_RoutingForms_Form>, "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<number, ReturnType<typeof getAttributes>>();
|
||||
|
||||
const [_, teamMembersMatchingAttributeLogicTimeTaken] = await aPf(async () => {
|
||||
return await async.mapLimit<TeamMemberWithAttributeOptionValuePerAttribute, Promise<void>>(
|
||||
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<ReturnValue>(fn: () => ReturnValue): [ReturnValue, number | null] {
|
||||
|
||||
Reference in New Issue
Block a user