Skip to content

Commit

Permalink
fix: module instance link in debugger (#3900)
Browse files Browse the repository at this point in the history
## Description
When a module instance is executed; under the hood the public entity of
the module is triggered and the evaluation is only aware of the entity
and not the instance. So whenever an error is raised, the error logs
contain the info around the underlying entity and not the module
instance. To bridge that gap; a helper transformer function is
introduced that would transform the entities to the right format if it
belongs to a module instance.

Fixes #31813

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!IMPORTANT]  
> Workflow run:
<https://github.com/appsmithorg/appsmith-ee/actions/runs/8627876403>
> Commit: `7e760125d2c9135130706ea70d8f7f403fc4201a`
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8627876403&attempt=2"
target="_blank">Click here!</a>
> All cypress tests have passed 🎉🎉🎉

<!-- end of auto-generated comment: Cypress test results  -->
  • Loading branch information
ashit-rath authored Apr 11, 2024
1 parent 046c86d commit c31881a
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
export * from "ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity";
import React from "react";
import type { LogItemProps } from "components/editorComponents/Debugger/ErrorLogs/ErrorLogItem";
import type { IconEntityMapper } from "ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity";
import { getIconForEntity as CE_getIconForEntity } from "ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity";
import { importRemixIcon } from "@design-system/widgets-old";
import { ENTITY_TYPE } from "@appsmith/entities/DataTree/types";
import { getModuleIcon } from "pages/Editor/utils";
import { useSelector } from "react-redux";
import { getModuleById } from "@appsmith/selectors/modulesSelector";
import { getModuleInstanceById } from "@appsmith/selectors/moduleInstanceSelectors";

const GuideLineIcon = importRemixIcon(
async () => import("remixicon-react/GuideLineIcon"),
);

export const getIconForEntity: Record<
string,
(props: LogItemProps, pluginImages: Record<string, string>) => any
> = {
export const getIconForEntity: IconEntityMapper = {
...CE_getIconForEntity,
[ENTITY_TYPE.MODULE_INPUT]: () => {
return <GuideLineIcon />;
},
[ENTITY_TYPE.MODULE_INSTANCE]: (props, pluginImages) => {
return getModuleIcon(undefined, pluginImages);
[ENTITY_TYPE.MODULE_INSTANCE]: (props) => {
const moduleInstance = useSelector((state) =>
getModuleInstanceById(state, props.id || ""),
);
const module = useSelector((state) =>
getModuleById(state, moduleInstance?.sourceModuleId || ""),
);
return getModuleIcon(module, props.pluginImages) || null;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export interface ModuleInstanceEntitiesReducerState {
jsCollections: ModuleInstanceJSCollectionData[];
}

export type ModuleInstanceAction =
ModuleInstanceEntitiesReducerState["actions"][number]["config"];
export type ModuleInstanceJSCollection =
ModuleInstanceEntitiesReducerState["jsCollections"][number]["config"];

export const initialState: ModuleInstanceEntitiesReducerState = {
actions: [],
jsCollections: [],
Expand Down
263 changes: 263 additions & 0 deletions app/client/src/ee/sagas/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import {
ENTITY_TYPE,
PLATFORM_ERROR,
} from "@appsmith/entities/AppsmithConsole/utils";
import { runSaga } from "redux-saga";
import { transformAddErrorLogsSaga } from "./helpers";
import { Severity, type Log, LOG_CATEGORY } from "entities/AppsmithConsole";
import { klona } from "klona";
import { set } from "lodash";
import { MODULE_TYPE } from "@appsmith/constants/ModuleConstants";

const queryModuleInstanceErrorLog: Log = {
id: "queryInstanceActionId",
iconId: "644b84d980127e0eff78a732",
logType: 2,
environmentName: "Production",
text: "Execution failed with status SQLSTATE: 42601",
source: {
type: ENTITY_TYPE.ACTION,
name: "QueryModule31",
id: "queryInstanceActionId",
},
messages: [
{
message: {
name: "PluginExecutionError",
message: "ERROR: syntax error at end of input\n Position: 35",
},
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: "INTERNAL_ERROR",
},
],
state: {
actionId: "queryInstanceActionId",
requestedAt: 1712125304920,
requestParams: {
Query: {
value: 'SELECT * FROM public."users" LIMIT;',
substitutedParams: {},
},
},
},
pluginErrorDetails: {
title: "Query execution error",
errorType: "INTERNAL_ERROR",
appsmithErrorCode: "PE-PGS-5000",
appsmithErrorMessage: "Your PostgreSQL query failed to execute.",
downstreamErrorCode: "SQLSTATE: 42601",
downstreamErrorMessage:
"ERROR: syntax error at end of input\n Position: 35",
},
severity: Severity.ERROR,
timestamp: "1712125307046",
occurrenceCount: 1,
category: LOG_CATEGORY.PLATFORM_GENERATED,
isExpanded: false,
};

const jsModuleInstanceErrorLog: Log = {
id: "moduleInstanceJSCollectionId-actionId",
logType: 5,
text: "JS Function execution failed: JSModule11.myFun1",
messages: [
{
message: {
name: "TypeError",
message: "x.asdkljas is not a function",
},
type: PLATFORM_ERROR.JS_FUNCTION_EXECUTION,
subType: "PARSE",
},
],
source: {
id: "moduleInstanceJSCollectionId",
name: "JSModule11",
type: ENTITY_TYPE.JSACTION,
propertyPath: "myFun1",
},
severity: Severity.ERROR,
timestamp: "1712132302018",
occurrenceCount: 1,
category: LOG_CATEGORY.PLATFORM_GENERATED,
isExpanded: false,
};

const DEFAULT_STATE = {
entities: {
moduleInstances: {
queryModuleInstanceId: {
id: "queryModuleInstanceId",
type: MODULE_TYPE.QUERY,
sourceModuleId: "queryModuleId",
name: "QueryModule31",
contextType: "PAGE",
contextId: "65fc1233b48e3e52a6d91d3b",
applicationId: "65fc1233b48e3e52a6d91d37",
workspaceId: "65fc11fdb48e3e52a6d91d30",
},
jsModuleInstanceId: {
id: "jsModuleInstanceId",
type: MODULE_TYPE.JS,
sourceModuleId: "jsModuleId",
name: "JSModule11",
contextType: "PAGE",
contextId: "65fc1233b48e3e52a6d91d3b",
applicationId: "65fc1233b48e3e52a6d91d37",
workspaceId: "65fc11fdb48e3e52a6d91d30",
},
},
moduleInstanceEntities: {
actions: [
{
config: {
id: "queryInstanceActionId",
moduleInstanceId: "queryModuleInstanceId",
},
},
],
jsCollections: [
{
config: {
id: "moduleInstanceJSCollectionId",
moduleInstanceId: "jsModuleInstanceId",
},
},
],
},
},
};

const widgetErrorLog = {
id: "j1vz86pd0v-defaultText",
iconId: "j1vz86pd0v",
logType: 5,
text: "The value at defaultText is invalid",
messages: [
{
message: {
name: "SyntaxError",
message: "Unexpected token '('",
},
type: "PARSE",
},
{
message: {
name: "TypeError",
message: "This value must be string",
},
type: "VALIDATION",
},
],
source: {
id: "j1vz86pd0v",
name: "Input1",
type: ENTITY_TYPE.WIDGET,
propertyPath: "defaultText",
pluginType: "INPUT_WIDGET_V2",
},
analytics: {
widgetType: "INPUT_WIDGET_V2",
},
} as unknown as Log;

const moduleInputErrorLog = {
id: "6603c72ce98dd96ec6fde480-input1",
iconId: "QUERY_MODULE",
logType: 5,
text: "The value at input1 is invalid",
messages: [
{
message: {
name: "ReferenceError",
message: "sss is not defined",
},
type: "PARSE",
},
],
source: {
id: "6603c72ce98dd96ec6fde480",
name: "inputs",
type: ENTITY_TYPE.MODULE_INPUT,
propertyPath: "input1",
},
analytics: {},
} as unknown as Log;

describe("transformAddErrorLogsSaga", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("transforms logs with ENTITY_TYPE.ACTION belonging to a module instance", async () => {
const logs: Log[] = [queryModuleInstanceErrorLog];
const result = await runSaga(
{
getState: () => DEFAULT_STATE,
},
transformAddErrorLogsSaga,
logs,
).toPromise();

const expectedResult = klona(logs);
expectedResult[0].id = "queryModuleInstanceId";
if (expectedResult[0].source) {
expectedResult[0].source.name = "QueryModule31";
expectedResult[0].source.type = ENTITY_TYPE.MODULE_INSTANCE;
expectedResult[0].source.id = "queryModuleInstanceId";
}

expect(result).toEqual(expectedResult);
});

it("doesn't transform logs with ENTITY_TYPE.ACTION not belonging to a module instance", async () => {
const actionLog = klona(queryModuleInstanceErrorLog);
actionLog.id = "action-id";
set(actionLog, "source.id", "action-id");
const logs: Log[] = [actionLog];
const result = await runSaga(
{
getState: () => DEFAULT_STATE,
},
transformAddErrorLogsSaga,
logs,
).toPromise();

expect(result).toEqual(logs);
});

it("transforms logs with ENTITY_TYPE.JSACTION belonging to a module instance", async () => {
const logs: Log[] = [jsModuleInstanceErrorLog];
const result = await runSaga(
{
getState: () => DEFAULT_STATE,
},
transformAddErrorLogsSaga,
logs,
).toPromise();

const expectedResult = klona(logs);
if (expectedResult[0].source) {
expectedResult[0].source.name = "JSModule11";
expectedResult[0].source.type = ENTITY_TYPE.MODULE_INSTANCE;
expectedResult[0].source.id = "jsModuleInstanceId";
}

expect(result).toEqual(expectedResult);
});

it("doesn't transform logs with ENTITY_TYPE.JSACTION not belonging to a module instance", async () => {
const logs: Log[] = [widgetErrorLog, moduleInputErrorLog];
const result = await runSaga(
{
getState: () => DEFAULT_STATE,
},
transformAddErrorLogsSaga,
logs,
).toPromise();

expect(result).toEqual(logs);
});

it("doesn't transform logs for any other entity type than JSACTION and ACTION", async () => {});
});
Loading

0 comments on commit c31881a

Please sign in to comment.