chore: fix issues in eval error logs sent to faro (#40285)

## Description
Errors sent from evaluation worker shows up as following in faro. This
entry is illegible and doesn't have the correct stack trace.
```
console.error: [object Object] [object Object] at ? (https://app.appsmith.com/src/instrumentations/console/instrumentation.ts:28:33) at log (https://app.appsmith.com/static/js/sagas/ErrorSagas.tsx:322:22)
```

This pr sends a reconstructed error object for eval errors and retains
the original stack trace of the error
```
error: Fetch computation cache failed!! 
at AppComputationCache.getCachedComputationResult (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:48831:11) 
at createDependencyMap (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:50756:98) 
at ? (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:49278:221) 
at profileAsyncFn (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:37451:21) 
at DataTreeEvaluator.setupFirstTree (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:49278:103)
at async profileAsyncFn (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:37451:15) 
at async evalTree (https://dev.appsmith.com/static/js/src_workers_Evaluation_handlers_evalTree_ts-node_modules_blueprintjs_core_lib_esm_components_-d37317.chunk.js:46384:38) 
at async asyncRequestMessageListener (https://dev.appsmith.com/static/js/evalWorker.chunk.js:236:16)
```



Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

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

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/14512303434>
> Commit: 3bbb8c1df4a731e63c2a0b3805d28ad96696ffd0
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14512303434&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 17 Apr 2025 10:31:54 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced error reporting across the application by including stack
traces with error messages, providing more detailed information for
debugging.

- **Bug Fixes**
- Improved error handling in various areas to ensure errors are
consistently captured and reported with additional context.

- **Chores**
- Updated internal logging mechanisms for better error tracking and
maintainability.
- Refined error reconstruction and logging processes for more accurate
error capture and reporting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Diljit 2025-04-17 16:25:05 +05:30 committed by GitHub
parent 882ad85e23
commit 14c7a41ba5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 82 additions and 21 deletions

View File

@ -15,6 +15,7 @@ import {
getWebInstrumentations,
type Faro,
InternalLoggerLevel,
LogLevel,
} from "@grafana/faro-react";
import {
FaroTraceExporter,
@ -57,7 +58,12 @@ if (isTracingEnabled()) {
],
ignoreUrls,
consoleInstrumentation: {
consoleErrorAsLog: false,
disabledLevels: [
LogLevel.DEBUG,
LogLevel.TRACE,
LogLevel.INFO,
LogLevel.LOG,
],
},
trackResources: true,
trackWebVitalsAttribution: true,

View File

@ -1,4 +1,5 @@
import { v4 as uuidv4 } from "uuid";
import { error as errorLogger } from "loglevel";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function captureException(exception: any, hint?: any): string {
@ -11,8 +12,7 @@ export function captureException(exception: any, hint?: any): string {
{ type: "error", context: context },
);
} catch (error) {
// eslint-disable-next-line
console.error(error);
errorLogger(error);
}
return eventId;

View File

@ -30,6 +30,7 @@ import type { AppState } from "ee/reducers";
import { toast } from "@appsmith/ads";
import { isDynamicEntity } from "ee/entities/DataTree/isDynamicEntity";
import { getEntityPayloadInfo } from "ee/utils/getEntityPayloadInfo";
import { reconstructErrorFromEvalError } from "./helper";
const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
@ -217,6 +218,8 @@ export function* evalErrorHandler(
}
errors.forEach((error) => {
const reconstructedError = reconstructErrorFromEvalError(error);
switch (error.type) {
case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: {
if (error.context) {
@ -232,7 +235,7 @@ export function* evalErrorHandler(
if (error.context.logToSentry) {
// Send the generic error message to sentry for better grouping
captureException(new Error(error.message), {
captureException(reconstructedError, {
errorName: "CyclicalDependencyError",
tags: {
node,
@ -265,22 +268,26 @@ export function* evalErrorHandler(
kind: "error",
});
log.error(error);
captureException(error, { errorName: "EvalTreeError" });
captureException(reconstructedError, { errorName: "EvalTreeError" });
break;
}
case EvalErrorTypes.BAD_UNEVAL_TREE_ERROR: {
log.error(error);
captureException(error, { errorName: "BadUnevalTreeError" });
captureException(reconstructedError, {
errorName: "BadUnevalTreeError",
});
break;
}
case EvalErrorTypes.EVAL_PROPERTY_ERROR: {
captureException(error, { errorName: "EvalPropertyError" });
captureException(reconstructedError, {
errorName: "EvalPropertyError",
});
log.error(error);
break;
}
case EvalErrorTypes.CLONE_ERROR: {
log.debug(error);
captureException(new Error(error.message), {
captureException(reconstructedError, {
errorName: "CloneError",
extra: {
request: error.context,
@ -296,14 +303,14 @@ export function* evalErrorHandler(
text: `${error.message} at: ${error.context?.propertyPath}`,
});
log.error(error);
captureException(error, {
captureException(reconstructedError, {
errorName: "ParseJSError",
entity: error.context,
});
break;
}
case EvalErrorTypes.EXTRACT_DEPENDENCY_ERROR: {
captureException(new Error(error.message), {
captureException(reconstructedError, {
errorName: "ExtractDependencyError",
extra: error.context,
});
@ -311,7 +318,9 @@ export function* evalErrorHandler(
}
case EvalErrorTypes.UPDATE_DATA_TREE_ERROR: {
// Log to Sentry with additional context
captureException(error, { errorName: "UpdateDataTreeError" });
captureException(reconstructedError, {
errorName: "UpdateDataTreeError",
});
// Log locally with error details
log.error(`Evaluation Error: ${error.message}`, {
type: error.type,
@ -320,7 +329,7 @@ export function* evalErrorHandler(
}
default: {
log.error(error);
captureException(error, { errorName: "UnknownEvalError" });
captureException(reconstructedError, { errorName: "UnknownEvalError" });
}
}
});

View File

@ -24,10 +24,7 @@ import type { LintTreeSagaRequestData } from "plugins/Linting/types";
import { evalErrorHandler } from "./EvalErrorHandler";
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
import { endSpan, startRootSpan } from "instrumentation/generateTraces";
export interface UpdateDataTreeMessageData {
workerResponse: EvalTreeResponseData;
}
import type { UpdateDataTreeMessageData } from "./types";
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -37,6 +37,7 @@ import {
} from "../constants/Datasource";
import { type Datasource, ToastMessageType } from "../entities/Datasource";
import { getNextEntityName } from "utils/AppsmithUtils";
import type { EvalError } from "utils/DynamicBindingUtils";
// function to extract all objects that have dynamic values
export const extractFetchDynamicValueFormConfigs = (
@ -53,6 +54,29 @@ export const extractFetchDynamicValueFormConfigs = (
return output;
};
/**
* Reconstructs an EvalError from a serialized error object. This attaches the correct stack trace and error type to the error.
* this is used to send the error to faro.
*
* @param serializedError - The serialized error object to reconstruct.
* @returns A reconstructed Error object.
*/
export function reconstructErrorFromEvalError(serializedError: EvalError) {
const error = new Error(serializedError.message);
if (serializedError.stack) {
error.stack = serializedError.stack;
}
if (serializedError.context) {
Object.assign(error, {
context: serializedError.context,
});
}
return error;
}
// Function to extract all the objects that have to fetch dynamic values
export const extractQueueOfValuesToBeFetched = (evalOutput: FormEvalOutput) => {
let output: Record<string, ConditionalOutput> = {};

View File

@ -0,0 +1,5 @@
import type { EvalTreeResponseData } from "workers/Evaluation/types";
export interface UpdateDataTreeMessageData {
workerResponse: EvalTreeResponseData;
}

View File

@ -427,6 +427,7 @@ export interface EvaluationError extends DataTreeError {
| PropertyEvaluationErrorType.VALIDATION;
originalBinding?: string;
kind?: Partial<PropertyEvaluationErrorKind>;
stack?: string;
}
export interface LintError extends DataTreeError {

View File

@ -210,7 +210,15 @@ export function saveResolvedFunctionsAndJSUpdates(
}
}
} catch (e) {
//if we need to push error as popup in case
dataTreeEvalRef.errors.push({
type: EvalErrorTypes.PARSE_JS_ERROR,
message: (e as Error).message,
stack: (e as Error).stack,
context: {
entityType: entity.ENTITY_TYPE,
propertyPath: entityName + ".body",
},
});
}
} else {
const parsedBody = {

View File

@ -7,7 +7,7 @@ import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget";
import { create } from "mutative";
import { klona } from "klona/json";
import type { WidgetEntity } from "plugins/Linting/lib/entity/WidgetEntity";
import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas";
import type { UpdateDataTreeMessageData } from "sagas/types";
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import * as evalTreeWithChanges from "./evalTreeWithChanges";
import { APP_MODE } from "entities/App";

View File

@ -7,7 +7,7 @@ import type {
} from "./types";
import { MessageType, sendMessage } from "utils/MessageUtil";
import { MAIN_THREAD_ACTION } from "ee/workers/Evaluation/evalWorkerActions";
import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas";
import type { UpdateDataTreeMessageData } from "sagas/types";
import {
generateOptimisedUpdatesAndSetPrevState,
getNewDataTreeUpdates,

View File

@ -307,6 +307,12 @@ export async function evalTree(
dataTreeEvaluator?.setPrevState(parsedUpdates[0].rhs);
} catch (e) {
updates = "[]";
errors.push({
type: EvalErrorTypes.EVAL_TREE_ERROR,
message: (e as Error).message,
stack: (e as Error).stack,
});
}
isNewTree = false;
} else {

View File

@ -6,7 +6,7 @@ import type { DataTree } from "entities/DataTree/dataTreeTypes";
import equal from "fast-deep-equal";
import { get, isObject, set } from "lodash";
import { isMoment } from "moment";
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
import { EvalErrorTypes, type EvalError } from "utils/DynamicBindingUtils";
import { create } from "mutative";
export const fn_keys: string = "__fn_keys__";
import { klona } from "klona/json";
@ -465,7 +465,7 @@ export const generateSerialisedUpdates = (
): {
serialisedUpdates: string;
updates: Diff<DataTree, DataTree>[];
error?: { type: string; message: string };
error?: EvalError;
} => {
const updates = generateOptimisedUpdates(
prevState,
@ -491,6 +491,7 @@ export const generateSerialisedUpdates = (
error: {
type: EvalErrorTypes.SERIALIZATION_ERROR,
message: (error as Error).message,
stack: (error as Error).stack,
},
};
}
@ -547,6 +548,7 @@ export function updatePrevState(
dataTreeEvaluator.errors.push({
type: EvalErrorTypes.UPDATE_DATA_TREE_ERROR,
message: error.message,
stack: error.stack,
});
}
}

View File

@ -1205,6 +1205,7 @@ export default class DataTreeEvaluator {
context: {
propertyPath: fullPropertyPath,
},
stack: (error as Error).stack,
});
evalPropertyValue = undefined;
}
@ -1408,6 +1409,7 @@ export default class DataTreeEvaluator {
this.errors.push({
type: EvalErrorTypes.EVAL_TREE_ERROR,
message: (error as Error).message,
stack: (error as Error).stack,
});
} finally {
// Restore the dataStore since it was a part of contextTree and prone to mutation.

View File

@ -109,6 +109,7 @@ export const extractInfoFromBindings = (
context: {
script: binding,
},
stack: (error as Error).stack,
};
return {