display event errors in the frontend using errorDisplay w/ burnettk

This commit is contained in:
jasquat 2023-04-19 15:20:19 -04:00
parent 6747d9df3d
commit 9254d19b74
10 changed files with 112 additions and 54 deletions

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: c95031498e62 Revision ID: e0510795b643
Revises: Revises:
Create Date: 2023-04-19 10:35:25.813002 Create Date: 2023-04-19 14:36:11.004444
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
from sqlalchemy.dialects import mysql from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'c95031498e62' revision = 'e0510795b643'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -468,6 +468,10 @@ def upgrade():
sa.Column('process_instance_event_id', sa.Integer(), nullable=False), sa.Column('process_instance_event_id', sa.Integer(), nullable=False),
sa.Column('message', sa.String(length=1024), nullable=False), sa.Column('message', sa.String(length=1024), nullable=False),
sa.Column('stacktrace', sa.Text(), nullable=False), sa.Column('stacktrace', sa.Text(), nullable=False),
sa.Column('task_line_number', sa.Integer(), nullable=True),
sa.Column('task_offset', sa.Integer(), nullable=True),
sa.Column('task_line_contents', sa.String(length=255), nullable=True),
sa.Column('task_trace', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['process_instance_event_id'], ['process_instance_event.id'], ), sa.ForeignKeyConstraint(['process_instance_event_id'], ['process_instance_event.id'], ),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )

View File

@ -1923,8 +1923,8 @@ lxml = "*"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "feature/new-task-states"
resolved_reference = "7211e67ee0dfecbabaeb7cec8f0e0373bd7cdc10" resolved_reference = "5a0fd2774fde20dd65dbb49ae67f22cad53df528"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
@ -2307,7 +2307,7 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = ">=3.9,<3.12" python-versions = ">=3.9,<3.12"
content-hash = "994c36ab39238500b4fd05bc1ccdd2d729dd5f66749ab77b1028371147bdf753" content-hash = "4ae3c31115f378193f33eb9b27d376fcf0f7c20fba54ed5c921f37a4778b8d09"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [

View File

@ -27,7 +27,7 @@ flask-marshmallow = "*"
flask-migrate = "*" flask-migrate = "*"
flask-restful = "*" flask-restful = "*"
werkzeug = "*" werkzeug = "*"
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"} SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/new-task-states"}
# SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "6cad2981712bb61eca23af1adfafce02d3277cb9"} # SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "6cad2981712bb61eca23af1adfafce02d3277cb9"}
# SpiffWorkflow = {develop = true, path = "../../SpiffWorkflow" } # SpiffWorkflow = {develop = true, path = "../../SpiffWorkflow" }
sentry-sdk = "^1.10" sentry-sdk = "^1.10"

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
@ -11,9 +12,14 @@ class ProcessInstanceErrorDetailModel(SpiffworkflowBaseDBModel):
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
process_instance_event_id: int = db.Column(ForeignKey("process_instance_event.id"), nullable=False, index=True) process_instance_event_id: int = db.Column(ForeignKey("process_instance_event.id"), nullable=False, index=True)
process_instance_event = relationship('ProcessInstanceEventModel') process_instance_event = relationship('ProcessInstanceEventModel') # type: ignore
message: str = db.Column(db.String(1024), nullable=False) message: str = db.Column(db.String(1024), nullable=False)
# this should be 65k in mysql # this should be 65k in mysql
stacktrace: str = db.Column(db.Text(), nullable=False) stacktrace: str = db.Column(db.Text(), nullable=False)
task_line_number: Optional[int] = db.Column(db.Integer)
task_offset: Optional[int] = db.Column(db.Integer)
task_line_contents: Optional[str] = db.Column(db.String(255))
task_trace: Optional[list] = db.Column(db.JSON)

View File

@ -131,6 +131,7 @@ def process_instance_run(
ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsNotEnqueuedError,
ProcessInstanceIsAlreadyLockedError, ProcessInstanceIsAlreadyLockedError,
) as e: ) as e:
# import pdb; pdb.set_trace()
ErrorHandlingService.handle_error(process_instance, e) ErrorHandlingService.handle_error(process_instance, e)
raise e raise e
except Exception as e: except Exception as e:
@ -138,6 +139,7 @@ def process_instance_run(
# FIXME: this is going to point someone to the wrong task - it's misinformation for errors in sub-processes. # FIXME: this is going to point someone to the wrong task - it's misinformation for errors in sub-processes.
# we need to recurse through all last tasks if the last task is a call activity or subprocess. # we need to recurse through all last tasks if the last task is a call activity or subprocess.
if processor is not None: if processor is not None:
# import pdb; pdb.set_trace()
task = processor.bpmn_process_instance.last_task task = processor.bpmn_process_instance.last_task
raise ApiError.from_task( raise ApiError.from_task(
error_code="unknown_exception", error_code="unknown_exception",

View File

@ -1,5 +1,7 @@
import copy import copy
import json import json
from spiffworkflow_backend.exceptions.api_error import ApiError
from SpiffWorkflow.exceptions import WorkflowTaskException # type: ignore
from flask import g from flask import g
from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel
import traceback import traceback
@ -158,7 +160,7 @@ class TaskService:
if task_model.state == "COMPLETED" or task_failed: if task_model.state == "COMPLETED" or task_failed:
event_type = ProcessInstanceEventType.task_completed.value event_type = ProcessInstanceEventType.task_completed.value
if task_failed: if task_failed or task_model.state == TaskState.ERROR:
event_type = ProcessInstanceEventType.task_failed.value event_type = ProcessInstanceEventType.task_failed.value
# FIXME: some failed tasks will currently not have either timestamp since we only hook into spiff when tasks complete # FIXME: some failed tasks will currently not have either timestamp since we only hook into spiff when tasks complete
@ -620,9 +622,24 @@ class TaskService:
stacktrace = traceback.format_exc()[0:63999] stacktrace = traceback.format_exc()[0:63999]
message = str(exception)[0:1023] message = str(exception)[0:1023]
task_line_number = None
task_line_contents = None
task_trace = None
task_offset = None
# import pdb; pdb.set_trace()
if isinstance(exception, WorkflowTaskException) or (isinstance(exception, ApiError) and exception.error_code == 'task_error'):
task_line_number = exception.line_number
task_line_contents = exception.error_line
task_trace = exception.task_trace
task_offset = exception.offset
process_instance_error_detail = ProcessInstanceErrorDetailModel( process_instance_error_detail = ProcessInstanceErrorDetailModel(
process_instance_event=process_instance_event, process_instance_event=process_instance_event,
message=message, message=message,
stacktrace=stacktrace, stacktrace=stacktrace,
task_line_number=task_line_number,
task_line_contents=task_line_contents,
task_trace=task_trace,
task_offset=task_offset,
) )
db.session.add(process_instance_error_detail) db.session.add(process_instance_error_detail)

View File

@ -138,6 +138,8 @@ class TaskModelSavingDelegate(EngineStepDelegate):
| TaskState.MAYBE | TaskState.MAYBE
| TaskState.LIKELY | TaskState.LIKELY
| TaskState.FUTURE | TaskState.FUTURE
| TaskState.STARTED
| TaskState.ERROR
): ):
# these will be removed from the parent and then ignored # these will be removed from the parent and then ignored
if waiting_spiff_task._has_state(TaskState.PREDICTED_MASK): if waiting_spiff_task._has_state(TaskState.PREDICTED_MASK):

View File

@ -1,5 +1,6 @@
import { Notification } from './Notification'; import { Notification } from './Notification';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import { ErrorForDisplay } from '../interfaces';
function errorDetailDisplay( function errorDetailDisplay(
errorObject: any, errorObject: any,
@ -18,57 +19,65 @@ function errorDetailDisplay(
return null; return null;
} }
export const childrenForErrorObject = (errorObject: ErrorForDisplay) => {
let sentryLinkTag = null;
if (errorObject.sentry_link) {
sentryLinkTag = (
<span>
{
': Find details about this error here (it may take a moment to become available): '
}
<a href={errorObject.sentry_link} target="_blank" rel="noreferrer">
{errorObject.sentry_link}
</a>
</span>
);
}
const message = <div>{errorObject.message}</div>;
const taskName = errorDetailDisplay(errorObject, 'task_name', 'Task Name');
const taskId = errorDetailDisplay(errorObject, 'task_id', 'Task ID');
const fileName = errorDetailDisplay(errorObject, 'file_name', 'File Name');
const lineNumber = errorDetailDisplay(
errorObject,
'line_number',
'Line Number'
);
const errorLine = errorDetailDisplay(errorObject, 'error_line', 'Context');
let taskTrace = null;
if (errorObject.task_trace && errorObject.task_trace.length > 1) {
taskTrace = (
<div className="error_info">
<span className="error_title">Call Activity Trace:</span>
{errorObject.task_trace.reverse().join(' -> ')}
</div>
);
}
return [
message,
<br />,
sentryLinkTag,
taskName,
taskId,
fileName,
lineNumber,
errorLine,
taskTrace,
];
};
export default function ErrorDisplay() { export default function ErrorDisplay() {
const errorObject = useAPIError().error; const errorObject = useAPIError().error;
const { removeError } = useAPIError(); const { removeError } = useAPIError();
let errorTag = null; let errorTag = null;
if (errorObject) {
let sentryLinkTag = null;
if (errorObject.sentry_link) {
sentryLinkTag = (
<span>
{
': Find details about this error here (it may take a moment to become available): '
}
<a href={errorObject.sentry_link} target="_blank" rel="noreferrer">
{errorObject.sentry_link}
</a>
</span>
);
}
const message = <div>{errorObject.message}</div>; if (errorObject) {
const title = 'Error:'; const title = 'Error:';
const taskName = errorDetailDisplay(errorObject, 'task_name', 'Task Name');
const taskId = errorDetailDisplay(errorObject, 'task_id', 'Task ID');
const fileName = errorDetailDisplay(errorObject, 'file_name', 'File Name');
const lineNumber = errorDetailDisplay(
errorObject,
'line_number',
'Line Number'
);
const errorLine = errorDetailDisplay(errorObject, 'error_line', 'Context');
let taskTrace = null;
if (errorObject.task_trace && errorObject.task_trace.length > 1) {
taskTrace = (
<div className="error_info">
<span className="error_title">Call Activity Trace:</span>
{errorObject.task_trace.reverse().join(' -> ')}
</div>
);
}
errorTag = ( errorTag = (
<Notification title={title} onClose={() => removeError()} type="error"> <Notification title={title} onClose={() => removeError()} type="error">
{message} <>{childrenForErrorObject(errorObject)}</>
<br />
{sentryLinkTag}
{taskName}
{taskId}
{fileName}
{lineNumber}
{errorLine}
{taskTrace}
</Notification> </Notification>
); );
} }

View File

@ -238,8 +238,9 @@ export interface ErrorForDisplay {
task_name?: string; task_name?: string;
task_id?: string; task_id?: string;
line_number?: number; line_number?: number;
error_line?: string;
file_name?: string; file_name?: string;
task_trace?: [string]; task_trace?: string[];
} }
export interface AuthenticationParam { export interface AuthenticationParam {
@ -301,6 +302,10 @@ export interface ProcessInstanceEventErrorDetail {
id: number; id: number;
message: string; message: string;
stacktrace: string; stacktrace: string;
task_line_contents?: string;
task_line_number?: number;
task_offset?: number;
task_trace?: string[];
} }
export interface ProcessInstanceLogEntry { export interface ProcessInstanceLogEntry {

View File

@ -32,12 +32,14 @@ import {
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { import {
ErrorForDisplay,
PermissionsToCheck, PermissionsToCheck,
ProcessInstanceEventErrorDetail, ProcessInstanceEventErrorDetail,
ProcessInstanceLogEntry, ProcessInstanceLogEntry,
} from '../interfaces'; } from '../interfaces';
import Filters from '../components/Filters'; import Filters from '../components/Filters';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
import { childrenForErrorObject } from '../components/ErrorDisplay';
type OwnProps = { type OwnProps = {
variant: string; variant: string;
@ -54,6 +56,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
const [taskTypes, setTaskTypes] = useState<string[]>([]); const [taskTypes, setTaskTypes] = useState<string[]>([]);
const [eventTypes, setEventTypes] = useState<string[]>([]); const [eventTypes, setEventTypes] = useState<string[]>([]);
const [eventForModal, setEventForModal] = const [eventForModal, setEventForModal] =
useState<ProcessInstanceLogEntry | null>(null); useState<ProcessInstanceLogEntry | null>(null);
const [eventErrorDetails, setEventErrorDetails] = const [eventErrorDetails, setEventErrorDetails] =
@ -150,15 +153,25 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
const errorEventModal = () => { const errorEventModal = () => {
if (eventForModal) { if (eventForModal) {
const modalHeading = `Event Error Details for`; const modalHeading = 'Event Error Details';
let errorMessageTag = ( let errorMessageTag = (
<Loading className="some-class" withOverlay={false} small /> <Loading className="some-class" withOverlay={false} small />
); );
if (eventErrorDetails) { if (eventErrorDetails) {
const errorForDisplay: ErrorForDisplay = {
message: eventErrorDetails.message,
task_name: eventForModal.task_definition_name,
task_id: eventForModal.task_definition_identifier,
line_number: eventErrorDetails.task_line_number,
error_line: eventErrorDetails.task_line_contents,
task_trace: eventErrorDetails.task_trace,
};
const errorChildren = childrenForErrorObject(errorForDisplay);
errorMessageTag = ( errorMessageTag = (
<> <>
<p className="failure-string">{eventErrorDetails.message} NOOO</p> <p className="failure-string">{eventErrorDetails.message}</p>
<br /> <br />
{errorChildren}
<pre>{eventErrorDetails.stacktrace}</pre> <pre>{eventErrorDetails.stacktrace}</pre>
</> </>
); );