display event errors in the frontend using errorDisplay w/ burnettk
This commit is contained in:
parent
49d954fdd6
commit
94414ea86b
|
@ -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')
|
||||||
)
|
)
|
|
@ -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 = [
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue