diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be20467..3f870dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,9 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Copy**: `Cmd/Ctrl+C` copies the selected frame name. - **Marker Navigation**: Click markers to select; arrow Left/Right to navigate between markers. - **Clear Selection**: Press `Escape` to deselect the current frame or marker. - - **Visual Display**: - - Time axis auto-spaces markers intelligently and more naturally as you zoom. - - Search + highlight dims non-matches for fast scanning. + - **⏱️ Time Axis Auto-Spacing**: Markers intelligently and naturally auto-space as you zoom. + - **🔍 Search + Highlight**: Dims non-matches for fast scanning. + - **Timeline Categories**: Redesigned timeline categories for clearer, more meaningful event grouping. ([#98]) + - Apex (Apex code), Automation (Workflow, NBA), Callout, Code Unit, DML, SOQL, System (System, Visualforce), Validation - **🎨 Themes**: - 18 curated timeline themes plus the default theme has been improved for better contrast and readability. - Add your own multiple custom themes via **Settings -> Apex Log Analyzer -> Timeline -> Custom Themes**. @@ -474,6 +475,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea +[#98]: https://github.com/certinia/debug-log-analyzer/issues/98 [#204]: https://github.com/certinia/debug-log-analyzer/issues/204 [#714]: https://github.com/certinia/debug-log-analyzer/issues/714 [#245]: https://github.com/certinia/debug-log-analyzer/issues/245 diff --git a/README.md b/README.md index ca441682..45f42d21 100644 --- a/README.md +++ b/README.md @@ -167,11 +167,27 @@ Seamlessly navigate between the visual analysis and your raw `.log` files: Adjust event colors in `settings.json`: ```json -"lana.timeline.colors": { - "Method": "#2B8F81", - "DML": "#285663", - "SOQL": "#5D4963", - ... +"lana.timeline.customThemes": { + "Glacial Warmth": { + "apex": "#6882A6", + "codeUnit": "#7A9B6E", + "system": "#9E8E7C", + "automation": "#D98650", + "dml": "#C85A5A", + "soql": "#57A89A", + "callout": "#C9A64D", + "validation": "#8B7BAC" + }, + "Orchid Slate": { + "apex": "#647C96", + "codeUnit": "#8872A8", + "system": "#8A7E7E", + "automation": "#C08545", + "dml": "#C94C6E", + "soql": "#5A9E85", + "callout": "#B5A044", + "validation": "#4EA6A6" + } } ``` diff --git a/apex-log-parser/__tests__/ApexLogParser.test.ts b/apex-log-parser/__tests__/ApexLogParser.test.ts index c43f88ae..e244752d 100644 --- a/apex-log-parser/__tests__/ApexLogParser.test.ts +++ b/apex-log-parser/__tests__/ApexLogParser.test.ts @@ -1120,7 +1120,7 @@ describe('Recalculate durations tests', () => { it('Recalculates parent node', () => { const parser = new ApexLogParser(); const node = new DummyLine(parser, ['14:32:07.563 (1)', 'DUMMY']); - node.subCategory = 'Method'; + node.category = 'Apex'; node.exitStamp = 3; node.recalculateDurations(); diff --git a/apex-log-parser/src/LogEvents.ts b/apex-log-parser/src/LogEvents.ts index 2762872d..a973dae6 100644 --- a/apex-log-parser/src/LogEvents.ts +++ b/apex-log-parser/src/LogEvents.ts @@ -5,14 +5,16 @@ import type { ApexLogParser, DebugLevel } from './ApexLogParser'; import type { CPUType, + DebugCategory, GovernorLimits, Limits, LineNumber, + LogCategory, LogEventType, LogIssue, - LogSubCategory, SelfTotal, } from './types.js'; +import { DEBUG_CATEGORY, LOG_CATEGORY } from './types.js'; /** * All log lines extend this base class. @@ -106,9 +108,14 @@ export abstract class LogEvent { exitStamp: number | null = null; /** - * The log sub category this event belongs to + * The timeline display category this event belongs to. */ - subCategory: LogSubCategory = ''; + category: LogCategory = ''; + + /** + * The original Salesforce debug log category. + */ + debugCategory: DebugCategory = ''; /** * The CPU type, e.g loading, method, custom @@ -269,13 +276,15 @@ export class DurationLogEvent extends LogEvent { parser: ApexLogParser, parts: string[], exitTypes: LogEventType[], - subCategory: LogSubCategory, + category: LogCategory, cpuType: CPUType, + debugCategory: DebugCategory = '', ) { super(parser, parts); this.exitTypes = exitTypes; - this.subCategory = subCategory; + this.category = category; this.cpuType = cpuType; + this.debugCategory = debugCategory; } } @@ -295,7 +304,7 @@ export class ApexLog extends LogEvent { timestamp = 0; exitStamp = 0; exitTypes = []; - subCategory: LogSubCategory = ''; + override category: LogCategory = ''; cpuType: CPUType = ''; /** @@ -427,7 +436,14 @@ export class BulkHeapAllocateLine extends LogEvent { export class CalloutRequestLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['CALLOUT_RESPONSE'], 'Method', 'free'); + super( + parser, + parts, + ['CALLOUT_RESPONSE'], + LOG_CATEGORY.Callout, + 'free', + DEBUG_CATEGORY.Callout, + ); this.text = parts[3] ?? ''; this.lineNumber = this.parseLineNumber(parts[2]); } @@ -445,6 +461,7 @@ export class CalloutResponseLine extends LogEvent { export class NamedCredentialRequestLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Callout; this.text = `${parts[3]} : ${parts[4]} : ${parts[5]} : ${parts[6]}`; } } @@ -452,6 +469,7 @@ export class NamedCredentialRequestLine extends LogEvent { export class NamedCredentialResponseLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Callout; this.text = `${parts[2]}`; } } @@ -459,6 +477,7 @@ export class NamedCredentialResponseLine extends LogEvent { export class NamedCredentialResponseDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Callout; this.text = `${parts[3]} : ${parts[4]} ${parts[5]} : ${parts[6]} ${parts[7]}`; } } @@ -468,7 +487,14 @@ export class ConstructorEntryLine extends DurationLogEvent { suffix = ' (constructor)'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['CONSTRUCTOR_EXIT'], 'Method', 'method'); + super( + parser, + parts, + ['CONSTRUCTOR_EXIT'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); this.lineNumber = this.parseLineNumber(parts[2]); const [, , , , args, className] = parts; @@ -517,7 +543,7 @@ export class MethodEntryLine extends DurationLogEvent { hasValidSymbols = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['METHOD_EXIT'], 'Method', 'method'); + super(parser, parts, ['METHOD_EXIT'], LOG_CATEGORY.Apex, 'method', DEBUG_CATEGORY.ApexCode); const [, , lineNumber, , methodName] = parts; this.lineNumber = this.parseLineNumber(lineNumber); this.text = methodName || this.type || this.text; @@ -592,7 +618,14 @@ export class SystemConstructorEntryLine extends DurationLogEvent { suffix = '(system constructor)'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SYSTEM_CONSTRUCTOR_EXIT'], 'System Method', 'method'); + super( + parser, + parts, + ['SYSTEM_CONSTRUCTOR_EXIT'], + LOG_CATEGORY.System, + 'method', + DEBUG_CATEGORY.System, + ); this.lineNumber = this.parseLineNumber(parts[2]); this.text = parts[3] || ''; } @@ -608,7 +641,14 @@ export class SystemConstructorExitLine extends LogEvent { } export class SystemMethodEntryLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SYSTEM_METHOD_EXIT'], 'System Method', 'method'); + super( + parser, + parts, + ['SYSTEM_METHOD_EXIT'], + LOG_CATEGORY.System, + 'method', + DEBUG_CATEGORY.System, + ); this.lineNumber = this.parseLineNumber(parts[2]); this.text = parts[3] || ''; } @@ -628,7 +668,14 @@ export class CodeUnitStartedLine extends DurationLogEvent { codeUnitType = ''; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['CODE_UNIT_FINISHED'], 'Code Unit', 'custom'); + super( + parser, + parts, + ['CODE_UNIT_FINISHED'], + LOG_CATEGORY.CodeUnit, + 'custom', + DEBUG_CATEGORY.ApexCode, + ); const typeString = parts[5] || parts[4] || parts[3] || ''; let sepIndex = typeString.indexOf(':'); @@ -713,7 +760,14 @@ export class VFApexCallStartLine extends DurationLogEvent { ]; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['VF_APEX_CALL_END'], 'Method', 'method'); + super( + parser, + parts, + ['VF_APEX_CALL_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); this.lineNumber = this.parseLineNumber(parts[2]); const classText = parts[5] || parts[3] || ''; @@ -760,7 +814,14 @@ export class VFApexCallEndLine extends LogEvent { export class VFDeserializeViewstateBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['VF_DESERIALIZE_VIEWSTATE_END'], 'System Method', 'method'); + super( + parser, + parts, + ['VF_DESERIALIZE_VIEWSTATE_END'], + LOG_CATEGORY.System, + 'method', + DEBUG_CATEGORY.Visualforce, + ); } } @@ -768,7 +829,14 @@ export class VFFormulaStartLine extends DurationLogEvent { suffix = ' (VF FORMULA)'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['VF_EVALUATE_FORMULA_END'], 'System Method', 'custom'); + super( + parser, + parts, + ['VF_EVALUATE_FORMULA_END'], + LOG_CATEGORY.System, + 'custom', + DEBUG_CATEGORY.Visualforce, + ); this.text = parts[3] || ''; } } @@ -778,13 +846,21 @@ export class VFFormulaEndLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Visualforce; this.text = parts[2] || ''; } } export class VFSeralizeViewStateStartLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['VF_SERIALIZE_VIEWSTATE_END'], 'System Method', 'method'); + super( + parser, + parts, + ['VF_SERIALIZE_VIEWSTATE_END'], + LOG_CATEGORY.System, + 'method', + DEBUG_CATEGORY.Visualforce, + ); } } @@ -792,6 +868,7 @@ export class VFPageMessageLine extends LogEvent { acceptsText = true; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.ApexCode; this.text = parts[2] || ''; } } @@ -804,7 +881,7 @@ export class DMLBeginLine extends DurationLogEvent { namespace = 'default'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['DML_END'], 'DML', 'free'); + super(parser, parts, ['DML_END'], LOG_CATEGORY.DML, 'free', DEBUG_CATEGORY.Database); this.lineNumber = this.parseLineNumber(parts[2]); this.text = 'DML ' + parts[3] + ' ' + parts[4]; const rowCountString = parts[5]; @@ -837,7 +914,7 @@ export class SOQLExecuteBeginLine extends DurationLogEvent { }; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SOQL_EXECUTE_END'], 'SOQL', 'free'); + super(parser, parts, ['SOQL_EXECUTE_END'], LOG_CATEGORY.SOQL, 'free', DEBUG_CATEGORY.Database); this.lineNumber = this.parseLineNumber(parts[2]); const [, , , aggregations, soqlString] = parts; @@ -918,7 +995,7 @@ export class SOSLExecuteBeginLine extends DurationLogEvent { }; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SOSL_EXECUTE_END'], 'SOQL', 'free'); + super(parser, parts, ['SOSL_EXECUTE_END'], LOG_CATEGORY.SOQL, 'free', DEBUG_CATEGORY.Database); this.lineNumber = this.parseLineNumber(parts[2]); this.text = `SOSL: ${parts[3]}`; } @@ -996,7 +1073,14 @@ export class UserDebugLine extends LogEvent { export class CumulativeLimitUsageLine extends DurationLogEvent { namespace = 'default'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['CUMULATIVE_LIMIT_USAGE_END'], 'System Method', 'system'); + super( + parser, + parts, + ['CUMULATIVE_LIMIT_USAGE_END'], + LOG_CATEGORY.System, + 'system', + DEBUG_CATEGORY.ApexProfiling, + ); } } @@ -1012,7 +1096,14 @@ export class CumulativeProfilingLine extends LogEvent { export class CumulativeProfilingBeginLine extends DurationLogEvent { namespace = 'default'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['CUMULATIVE_PROFILING_END'], 'System Method', 'custom'); + super( + parser, + parts, + ['CUMULATIVE_PROFILING_END'], + LOG_CATEGORY.System, + 'custom', + DEBUG_CATEGORY.ApexProfiling, + ); } } @@ -1020,6 +1111,7 @@ export class LimitUsageLine extends LogEvent { namespace = 'default'; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.ApexProfiling; this.lineNumber = this.parseLineNumber(parts[2]); this.text = parts[3] + ' ' + parts[4] + ' out of ' + parts[5]; } @@ -1046,6 +1138,7 @@ export class LimitUsageForNSLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.ApexProfiling; this.acceptsText = true; this.text = parts[2] || ''; } @@ -1109,7 +1202,7 @@ export class LimitUsageForNSLine extends LogEvent { export class NBANodeBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['NBA_NODE_END'], 'System Method', 'method'); + super(parser, parts, ['NBA_NODE_END'], LOG_CATEGORY.Automation, 'method', DEBUG_CATEGORY.NBA); this.text = parts.slice(2).join(' | '); } } @@ -1141,7 +1234,14 @@ export class NBAOfferInvalid extends LogEvent { } export class NBAStrategyBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['NBA_STRATEGY_END'], 'System Method', 'method'); + super( + parser, + parts, + ['NBA_STRATEGY_END'], + LOG_CATEGORY.Automation, + 'method', + DEBUG_CATEGORY.NBA, + ); this.text = parts.slice(2).join(' | '); } } @@ -1177,7 +1277,7 @@ export class PopTraceFlagsLine extends LogEvent { export class QueryMoreBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['QUERY_MORE_END'], 'SOQL', 'custom'); + super(parser, parts, ['QUERY_MORE_END'], LOG_CATEGORY.SOQL, 'custom', DEBUG_CATEGORY.Database); this.lineNumber = this.parseLineNumber(parts[2]); this.text = `line: ${this.lineNumber}`; } @@ -1259,13 +1359,20 @@ export class SystemModeExitLine extends LogEvent { export class ExecutionStartedLine extends DurationLogEvent { namespace = 'default'; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['EXECUTION_FINISHED'], 'Method', 'method'); + super( + parser, + parts, + ['EXECUTION_FINISHED'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class EnteringManagedPackageLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, [], 'Method', 'pkg'); + super(parser, parts, [], LOG_CATEGORY.Apex, 'pkg', DEBUG_CATEGORY.ApexCode); const rawNs = parts[2] || '', lastDot = rawNs.lastIndexOf('.'); @@ -1281,7 +1388,14 @@ export class EnteringManagedPackageLine extends DurationLogEvent { export class EventSericePubBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['EVENT_SERVICE_PUB_END'], 'Flow', 'custom'); + super( + parser, + parts, + ['EVENT_SERVICE_PUB_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[2] || ''; } } @@ -1304,7 +1418,14 @@ export class EventSericePubDetailLine extends LogEvent { export class EventSericeSubBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['EVENT_SERVICE_SUB_END'], 'Flow', 'custom'); + super( + parser, + parts, + ['EVENT_SERVICE_SUB_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} ${parts[3]}`; } } @@ -1329,7 +1450,14 @@ export class FlowStartInterviewsBeginLine extends DurationLogEvent { text = 'FLOW_START_INTERVIEWS : '; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['FLOW_START_INTERVIEWS_END'], 'Flow', 'custom'); + super( + parser, + parts, + ['FLOW_START_INTERVIEWS_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); } onEnd(end: LogEvent, stack: LogEvent[]) { @@ -1369,13 +1497,21 @@ export class FlowStartInterviewsErrorLine extends LogEvent { acceptsText = true; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} - ${parts[4]}`; } } export class FlowStartInterviewBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['FLOW_START_INTERVIEW_END'], 'Flow', 'custom'); + super( + parser, + parts, + ['FLOW_START_INTERVIEW_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[3] || ''; } } @@ -1383,6 +1519,7 @@ export class FlowStartInterviewBeginLine extends DurationLogEvent { export class FlowStartInterviewLimitUsageLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1390,6 +1527,7 @@ export class FlowStartInterviewLimitUsageLine extends LogEvent { export class FlowStartScheduledRecordsLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]}`; } } @@ -1397,13 +1535,21 @@ export class FlowStartScheduledRecordsLine extends LogEvent { export class FlowCreateInterviewErrorLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } export class FlowElementBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['FLOW_ELEMENT_END'], 'Flow', 'custom'); + super( + parser, + parts, + ['FLOW_ELEMENT_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[3] + ' ' + parts[4]; } } @@ -1411,6 +1557,7 @@ export class FlowElementBeginLine extends DurationLogEvent { export class FlowElementDeferredLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] + ' ' + parts[3]; } } @@ -1420,6 +1567,7 @@ export class FlowElementAssignmentLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[3] + ' ' + parts[4]; } } @@ -1427,6 +1575,7 @@ export class FlowElementAssignmentLine extends LogEvent { export class FlowWaitEventResumingDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } @@ -1434,6 +1583,7 @@ export class FlowWaitEventResumingDetailLine extends LogEvent { export class FlowWaitEventWaitingDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]} : ${parts[6]}`; } } @@ -1441,6 +1591,7 @@ export class FlowWaitEventWaitingDetailLine extends LogEvent { export class FlowWaitResumingDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1448,6 +1599,7 @@ export class FlowWaitResumingDetailLine extends LogEvent { export class FlowWaitWaitingDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } @@ -1455,6 +1607,7 @@ export class FlowWaitWaitingDetailLine extends LogEvent { export class FlowInterviewFinishedLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[3] || ''; } } @@ -1462,6 +1615,7 @@ export class FlowInterviewFinishedLine extends LogEvent { export class FlowInterviewResumedLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]}`; } } @@ -1469,6 +1623,7 @@ export class FlowInterviewResumedLine extends LogEvent { export class FlowInterviewPausedLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1477,6 +1632,7 @@ export class FlowElementErrorLine extends LogEvent { acceptsText = true; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[1] || '' + parts[2] + ' ' + parts[3] + ' ' + parts[4]; } } @@ -1484,6 +1640,7 @@ export class FlowElementErrorLine extends LogEvent { export class FlowElementFaultLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1491,6 +1648,7 @@ export class FlowElementFaultLine extends LogEvent { export class FlowElementLimitUsageLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]}`; } } @@ -1498,6 +1656,7 @@ export class FlowElementLimitUsageLine extends LogEvent { export class FlowInterviewFinishedLimitUsageLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]}`; } } @@ -1505,6 +1664,7 @@ export class FlowInterviewFinishedLimitUsageLine extends LogEvent { export class FlowSubflowDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } @@ -1512,6 +1672,7 @@ export class FlowSubflowDetailLine extends LogEvent { export class FlowActionCallDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[3] + ' : ' + parts[4] + ' : ' + parts[5] + ' : ' + parts[6]; } } @@ -1519,6 +1680,7 @@ export class FlowActionCallDetailLine extends LogEvent { export class FlowAssignmentDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[3] + ' : ' + parts[4] + ' : ' + parts[5]; } } @@ -1526,6 +1688,7 @@ export class FlowAssignmentDetailLine extends LogEvent { export class FlowLoopDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[3] + ' : ' + parts[4]; } } @@ -1533,13 +1696,21 @@ export class FlowLoopDetailLine extends LogEvent { export class FlowRuleDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[3] + ' : ' + parts[4]; } } export class FlowBulkElementBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['FLOW_BULK_ELEMENT_END'], 'Flow', 'custom'); + super( + parser, + parts, + ['FLOW_BULK_ELEMENT_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} - ${parts[3]}`; } } @@ -1547,6 +1718,7 @@ export class FlowBulkElementBeginLine extends DurationLogEvent { export class FlowBulkElementDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] + ' : ' + parts[3] + ' : ' + parts[4]; } } @@ -1554,6 +1726,7 @@ export class FlowBulkElementDetailLine extends LogEvent { export class FlowBulkElementNotSupportedLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1561,6 +1734,7 @@ export class FlowBulkElementNotSupportedLine extends LogEvent { export class FlowBulkElementLimitUsageLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1624,12 +1798,14 @@ export class TestingLimitsLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.ApexProfiling; } } export class ValidationRuleLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Validation; this.text = parts[3] || ''; } } @@ -1638,6 +1814,7 @@ export class ValidationErrorLine extends LogEvent { acceptsText = true; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Validation; this.text = parts[2] || ''; } } @@ -1647,6 +1824,7 @@ export class ValidationFormulaLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Validation; const extra = parts.length > 3 ? ' ' + parts[3] : ''; this.text = parts[2] + extra; @@ -1656,6 +1834,7 @@ export class ValidationFormulaLine extends LogEvent { export class ValidationPassLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Validation; this.text = parts[3] || ''; } } @@ -1664,6 +1843,7 @@ export class WFFlowActionErrorLine extends LogEvent { acceptsText = true; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[1] + ' ' + parts[4]; } } @@ -1672,6 +1852,7 @@ export class WFFlowActionErrorDetailLine extends LogEvent { acceptsText = true; constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[1] + ' ' + parts[2]; } } @@ -1680,14 +1861,28 @@ export class WFFieldUpdateLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_FIELD_UPDATE'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_FIELD_UPDATE'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = ' ' + parts[2] + ' ' + parts[3] + ' ' + parts[4] + ' ' + parts[5] + ' ' + parts[6]; } } export class WFRuleEvalBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_RULE_EVAL_END'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_RULE_EVAL_END'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[2] || ''; } } @@ -1695,6 +1890,7 @@ export class WFRuleEvalBeginLine extends DurationLogEvent { export class WFRuleEvalValueLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1704,13 +1900,21 @@ export class WFRuleFilterLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } export class WFCriteriaBeginLine extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_CRITERIA_END', 'WF_RULE_NOT_EVALUATED'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_CRITERIA_END', 'WF_RULE_NOT_EVALUATED'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = 'WF_CRITERIA : ' + parts[5] + ' : ' + parts[3]; } } @@ -1721,7 +1925,14 @@ export class WFFormulaLine extends DurationLogEvent { nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_FORMULA'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_FORMULA'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[2] + ' : ' + parts[3]; } } @@ -1729,6 +1940,7 @@ export class WFFormulaLine extends DurationLogEvent { export class WFActionLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1736,6 +1948,7 @@ export class WFActionLine extends LogEvent { export class WFActionsEndLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1743,6 +1956,7 @@ export class WFActionsEndLine extends LogEvent { export class WFActionTaskLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]} : ${parts[6]} : ${parts[7]}`; } } @@ -1751,7 +1965,14 @@ export class WFApprovalLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_APPROVAL'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_APPROVAL'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1759,6 +1980,7 @@ export class WFApprovalLine extends DurationLogEvent { export class WFApprovalRemoveLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]}`; } } @@ -1767,7 +1989,14 @@ export class WFApprovalSubmitLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_APPROVAL_SUBMIT'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_APPROVAL_SUBMIT'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]}`; } } @@ -1775,6 +2004,7 @@ export class WFApprovalSubmitLine extends DurationLogEvent { export class WFApprovalSubmitterLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1782,6 +2012,7 @@ export class WFApprovalSubmitterLine extends LogEvent { export class WFAssignLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]}`; } } @@ -1790,7 +2021,14 @@ export class WFEmailAlertLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_EMAIL_ALERT'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_EMAIL_ALERT'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1799,7 +2037,14 @@ export class WFEmailSentLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_EMAIL_SENT'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_EMAIL_SENT'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1807,6 +2052,7 @@ export class WFEmailSentLine extends DurationLogEvent { export class WFEnqueueActionsLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1814,6 +2060,7 @@ export class WFEnqueueActionsLine extends LogEvent { export class WFEscalationActionLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]}`; } } @@ -1822,7 +2069,14 @@ export class WFEvalEntryCriteriaLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_EVAL_ENTRY_CRITERIA'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_EVAL_ENTRY_CRITERIA'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1830,6 +2084,7 @@ export class WFEvalEntryCriteriaLine extends DurationLogEvent { export class WFFlowActionDetailLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; const optional = parts[4] ? ` : ${parts[4]} :${parts[5]}` : ''; this.text = `${parts[2]} : ${parts[3]}` + optional; } @@ -1839,7 +2094,14 @@ export class WFNextApproverLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_NEXT_APPROVER'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_NEXT_APPROVER'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} : ${parts[3]} : ${parts[4]}`; } } @@ -1847,6 +2109,7 @@ export class WFNextApproverLine extends DurationLogEvent { export class WFOutboundMsgLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } @@ -1855,7 +2118,14 @@ export class WFProcessFoundLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_PROCESS_FOUND'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_PROCESS_FOUND'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = `${parts[2]} : ${parts[3]}`; } } @@ -1864,7 +2134,14 @@ export class WFProcessNode extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_PROCESS_NODE'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_PROCESS_NODE'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[2] || ''; } } @@ -1872,6 +2149,7 @@ export class WFProcessNode extends DurationLogEvent { export class WFReassignRecordLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]}`; } } @@ -1879,6 +2157,7 @@ export class WFReassignRecordLine extends LogEvent { export class WFResponseNotifyLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } @@ -1886,6 +2165,7 @@ export class WFResponseNotifyLine extends LogEvent { export class WFRuleEntryOrderLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1894,7 +2174,14 @@ export class WFRuleInvocationLine extends DurationLogEvent { isExit = true; nextLineIsExit = true; constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['WF_RULE_INVOCATION'], 'Workflow', 'custom'); + super( + parser, + parts, + ['WF_RULE_INVOCATION'], + LOG_CATEGORY.Automation, + 'custom', + DEBUG_CATEGORY.Workflow, + ); this.text = parts[2] || ''; } } @@ -1902,6 +2189,7 @@ export class WFRuleInvocationLine extends DurationLogEvent { export class WFSoftRejectLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1909,6 +2197,7 @@ export class WFSoftRejectLine extends LogEvent { export class WFTimeTriggerLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = `${parts[2]} : ${parts[3]} : ${parts[4]} : ${parts[5]}`; } } @@ -1916,6 +2205,7 @@ export class WFTimeTriggerLine extends LogEvent { export class WFSpoolActionBeginLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); + this.debugCategory = DEBUG_CATEGORY.Workflow; this.text = parts[2] || ''; } } @@ -1990,7 +2280,14 @@ export class XDSResponseErrorLine extends LogEvent { // e.g. "09:45:31.888 (38889007737)|DUPLICATE_DETECTION_BEGIN" export class DuplicateDetectionBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['DUPLICATE_DETECTION_END'], 'Workflow', 'custom'); + super( + parser, + parts, + ['DUPLICATE_DETECTION_END'], + LOG_CATEGORY.System, + 'custom', + DEBUG_CATEGORY.System, + ); } } @@ -2035,53 +2332,116 @@ export class DuplicateDetectionSummary extends LogEvent { export class SessionCachePutBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SESSION_CACHE_PUT_END'], 'Method', 'method'); + super( + parser, + parts, + ['SESSION_CACHE_PUT_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class SessionCacheGetBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SESSION_CACHE_GET_END'], 'Method', 'method'); + super( + parser, + parts, + ['SESSION_CACHE_GET_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class SessionCacheRemoveBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['SESSION_CACHE_REMOVE_END'], 'Method', 'method'); + super( + parser, + parts, + ['SESSION_CACHE_REMOVE_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class OrgCachePutBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['ORG_CACHE_PUT_END'], 'Method', 'method'); + super( + parser, + parts, + ['ORG_CACHE_PUT_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class OrgCacheGetBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['ORG_CACHE_GET_END'], 'Method', 'method'); + super( + parser, + parts, + ['ORG_CACHE_GET_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class OrgCacheRemoveBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['ORG_CACHE_REMOVE_END'], 'Method', 'method'); + super( + parser, + parts, + ['ORG_CACHE_REMOVE_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class VFSerializeContinuationStateBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['VF_SERIALIZE_CONTINUATION_STATE_END'], 'Method', 'method'); + super( + parser, + parts, + ['VF_SERIALIZE_CONTINUATION_STATE_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class VFDeserializeContinuationStateBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['VF_SERIALIZE_CONTINUATION_STATE_END'], 'Method', 'method'); + super( + parser, + parts, + ['VF_SERIALIZE_CONTINUATION_STATE_END'], + LOG_CATEGORY.Apex, + 'method', + DEBUG_CATEGORY.ApexCode, + ); } } export class MatchEngineBegin extends DurationLogEvent { constructor(parser: ApexLogParser, parts: string[]) { - super(parser, parts, ['MATCH_ENGINE_END'], 'Method', 'method'); + super( + parser, + parts, + ['MATCH_ENGINE_END'], + LOG_CATEGORY.System, + 'method', + DEBUG_CATEGORY.System, + ); } } diff --git a/apex-log-parser/src/index.ts b/apex-log-parser/src/index.ts index 3df4d182..5442c395 100644 --- a/apex-log-parser/src/index.ts +++ b/apex-log-parser/src/index.ts @@ -8,11 +8,13 @@ export { ApexLogParser, DebugLevel, parse } from './ApexLogParser.js'; // Types export type { CPUType, + DebugCategory, GovernorLimits, GovernorSnapshot, IssueType, Limits, LineNumber, + LogCategory, LogEventType, LogIssue, LogLineConstructor, @@ -20,6 +22,9 @@ export type { SelfTotal, } from './types.js'; +// Constants +export { ALL_LOG_CATEGORIES, DEBUG_CATEGORY, LOG_CATEGORY } from './types.js'; + // Events - classes and utilities used by consumers export { ApexLog, diff --git a/apex-log-parser/src/types.ts b/apex-log-parser/src/types.ts index a686870e..4eca7878 100644 --- a/apex-log-parser/src/types.ts +++ b/apex-log-parser/src/types.ts @@ -8,15 +8,48 @@ export type IssueType = 'unexpected' | 'error' | 'skip'; export type LineNumber = number | 'EXTERNAL' | null; // an actual line-number or 'EXTERNAL' -export type LogSubCategory = - | '' - | 'Method' - | 'System Method' - | 'Code Unit' - | 'DML' - | 'SOQL' - | 'Flow' - | 'Workflow'; +/** + * Original Salesforce debug log categories as defined in SF Setup > Debug Log Levels. + * These are the categories users configure in the Salesforce UI. + * See: https://help.salesforce.com/s/articleView?id=platform.code_setting_debug_log_levels.htm + */ +export const DEBUG_CATEGORY = { + Database: 'Database', + Workflow: 'Workflow', + NBA: 'NBA', + Validation: 'Validation', + Callout: 'Callout', + ApexCode: 'Apex Code', + ApexProfiling: 'Apex Profiling', + Visualforce: 'Visualforce', + System: 'System', +} as const; + +/** Original Salesforce debug log category (from Debug Log Levels UI). */ +export type DebugCategory = (typeof DEBUG_CATEGORY)[keyof typeof DEBUG_CATEGORY] | ''; + +/** + * Timeline display categories - our simplified/enhanced view of SF categories. + * Split Database → DML + SOQL, merge Flow + Workflow → Automation. + */ +export const LOG_CATEGORY = { + Apex: 'Apex', + System: 'System', + CodeUnit: 'Code Unit', + Automation: 'Automation', + DML: 'DML', + SOQL: 'SOQL', + Validation: 'Validation', + Callout: 'Callout', +} as const; + +export type LogCategory = (typeof LOG_CATEGORY)[keyof typeof LOG_CATEGORY] | ''; + +/** Readonly array of all category values (for building Sets, iterating, etc.) */ +export const ALL_LOG_CATEGORIES: readonly LogCategory[] = Object.values(LOG_CATEGORY); + +/** @deprecated Use LogCategory instead */ +export type LogSubCategory = LogCategory; export interface Limits { soqlQueries: { used: number; limit: number }; diff --git a/lana-docs-site/docs/docs/features/timeline.md b/lana-docs-site/docs/docs/features/timeline.md index 4be21bd0..43b0784d 100644 --- a/lana-docs-site/docs/docs/features/timeline.md +++ b/lana-docs-site/docs/docs/features/timeline.md @@ -39,7 +39,7 @@ The minimap gives you instant context of your entire log. Spot hotspots at a gla - **Skyline Chart**: A density-based visualization: - **Height** = maximum call stack depth at that point - - **Color** = dominant event category (method, SOQL, DML, etc.) + - **Color** = dominant event category (Apex, SOQL, DML, etc.) - **Opacity** = event density (brighter = more events) - **Viewport Lens**: A window showing exactly what's visible in the main timeline (time range horizontal, depth range vertical) - **Time Axis**: Time reference markers at the top @@ -333,23 +333,25 @@ You can create custom color themes to match your preferences or specific use cas ```json "lana.timeline.customThemes": { - "My Theme": { - "codeUnit": "#0176D3", - "workflow": "#CE4A6B", - "method": "#54698D", - "flow": "#9050E9", - "dml": "#D68128", - "soql": "#04844B", - "system": "#706E6B" + "Glacial Warmth": { + "apex": "#6882A6", + "codeUnit": "#7A9B6E", + "system": "#9E8E7C", + "automation": "#D98650", + "dml": "#C85A5A", + "soql": "#57A89A", + "callout": "#C9A64D", + "validation": "#8B7BAC" }, - "High Contrast": { - "codeUnit": "#722ED1", - "workflow": "#52C41A", - "method": "#1890FF", - "flow": "#00BCD4", - "dml": "#FF9100", - "soql": "#EB2F96", - "system": "#90A4AE" + "Orchid Slate": { + "apex": "#647C96", + "codeUnit": "#8872A8", + "system": "#8A7E7E", + "automation": "#C08545", + "dml": "#C94C6E", + "soql": "#5A9E85", + "callout": "#B5A044", + "validation": "#4EA6A6" } } ``` @@ -358,13 +360,14 @@ You can create custom color themes to match your preferences or specific use cas Each theme requires the following color properties (in hex format): +- **apex** - Apex method entry/exit events - **codeUnit** - Code Unit events -- **workflow** - Workflow and automation events -- **method** - Method entry/exit events -- **flow** - Flow execution events +- **system** - System method calls +- **automation** - Workflow, Flow, and Process Builder events - **dml** - DML operations (insert, update, delete, etc.) - **soql** - SOQL queries -- **system** - System method calls +- **callout** - HTTP callout events +- **validation** - Validation rule events Custom themes will appear in the theme selector alongside built-in themes and can be switched using the Command Palette or settings. diff --git a/lana-docs-site/docs/docs/settings.md b/lana-docs-site/docs/docs/settings.md index bada8fe9..4f0e90e1 100644 --- a/lana-docs-site/docs/docs/settings.md +++ b/lana-docs-site/docs/docs/settings.md @@ -31,13 +31,26 @@ or settings.json ```json -"lana.timeline.colors": { - "Code Unit": "#88AE58", - "Workflow": "#51A16E", - "Method": "#2B8F81", - "Flow": "#337986", - "DML": "#285663", - "SOQL": "#5D4963", - "System Method": "#5C3444" +"lana.timeline.customThemes": { + "Glacial Warmth": { + "apex": "#6882A6", + "codeUnit": "#7A9B6E", + "system": "#9E8E7C", + "automation": "#D98650", + "dml": "#C85A5A", + "soql": "#57A89A", + "callout": "#C9A64D", + "validation": "#8B7BAC" + }, + "Orchid Slate": { + "apex": "#647C96", + "codeUnit": "#8872A8", + "system": "#8A7E7E", + "automation": "#C08545", + "dml": "#C94C6E", + "soql": "#5A9E85", + "callout": "#B5A044", + "validation": "#4EA6A6" + } } ``` diff --git a/lana/package.json b/lana/package.json index 34015d61..22f99c7b 100644 --- a/lana/package.json +++ b/lana/package.json @@ -167,56 +167,59 @@ "order": 1, "default": { "Custom": { + "apex": "#2B8F81", "codeUnit": "#88AE58", - "workflow": "#51A16E", - "method": "#2B8F81", - "flow": "#5C8FA6", + "system": "#8D6E63", + "automation": "#51A16E", "dml": "#B06868", "soql": "#6D4C7D", - "system": "#8D6E63" + "callout": "#CCA033", + "validation": "#5C8FA6" } }, "additionalProperties": { "type": "object", "additionalProperties": false, "required": [ + "apex", "codeUnit", - "workflow", - "method", - "flow", + "system", + "automation", "dml", "soql", - "system" + "callout", + "validation" ], "properties": { "default": { + "apex": "#2B8F81", "codeUnit": "#88AE58", - "workflow": "#51A16E", - "method": "#2B8F81", - "flow": "#5C8FA6", + "system": "#8D6E63", + "automation": "#51A16E", "dml": "#B06868", "soql": "#6D4C7D", - "system": "#8D6E63" + "callout": "#CCA033", + "validation": "#5C8FA6" }, - "codeUnit": { + "apex": { "type": "string", "format": "color-hex", - "default": "#88AE58" + "default": "#2B8F81" }, - "workflow": { + "codeUnit": { "type": "string", "format": "color-hex", - "default": "#51A16E" + "default": "#88AE58" }, - "method": { + "system": { "type": "string", "format": "color-hex", - "default": "#2B8F81" + "default": "#8D6E63" }, - "flow": { + "automation": { "type": "string", "format": "color-hex", - "default": "#5C8FA6" + "default": "#51A16E" }, "dml": { "type": "string", @@ -228,10 +231,15 @@ "format": "color-hex", "default": "#6D4C7D" }, - "system": { + "callout": { "type": "string", "format": "color-hex", - "default": "#8D6E63" + "default": "#CCA033" + }, + "validation": { + "type": "string", + "format": "color-hex", + "default": "#5C8FA6" } } } @@ -253,7 +261,6 @@ "Code Unit": "#88AE58", "Workflow": "#51A16E", "Method": "#2B8F81", - "Flow": "#5C8FA6", "DML": "#B06868", "SOQL": "#6D4C7D", "System Method": "#8D6E63" @@ -277,12 +284,6 @@ "description": "Hex color for Method timeline events.", "format": "color-hex" }, - "Flow": { - "type": "string", - "default": "#5C8FA6", - "description": "Hex color for Flow timeline events.", - "format": "color-hex" - }, "DML": { "type": "string", "default": "#B06868", diff --git a/lana/src/__tests__/helpers/test-builders.ts b/lana/src/__tests__/helpers/test-builders.ts index 1000a828..5c8d6482 100644 --- a/lana/src/__tests__/helpers/test-builders.ts +++ b/lana/src/__tests__/helpers/test-builders.ts @@ -59,7 +59,7 @@ export function createMockLogEvent(overrides: PartialLogEvent = {}): LogEvent { discontinuity: false, timestamp: 1000000, exitStamp: 2000000, - subCategory: 'Method' as const, + category: 'Method' as const, cpuType: 'method' as const, duration: { self: 1000000, total: 1000000 }, dmlRowCount: { self: 0, total: 0 }, @@ -112,7 +112,7 @@ export function createMockApexLog(overrides: PartialApexLog = {}): ApexLog { discontinuity: false, timestamp: 0, exitStamp: 0, - subCategory: '' as const, + category: '' as const, cpuType: '' as const, duration: { self: 0, total: 0 }, dmlRowCount: { self: 0, total: 0 }, diff --git a/log-viewer/src/__tests__/soql/SOQLLinter.test.ts b/log-viewer/src/__tests__/soql/SOQLLinter.test.ts index a5262d48..7aec77c3 100644 --- a/log-viewer/src/__tests__/soql/SOQLLinter.test.ts +++ b/log-viewer/src/__tests__/soql/SOQLLinter.test.ts @@ -7,7 +7,7 @@ import { SOQLLinter } from '../../features/soql/services/SOQLLinter.js'; class DummySOQLLine extends LogEvent { constructor(parser: ApexLogParser, parts: string[]) { super(parser, parts); - this.subCategory = 'Code Unit'; + this.category = 'Code Unit'; this.cpuType = 'method'; this.exitTypes = ['CODE_UNIT_FINISHED']; } diff --git a/log-viewer/src/features/settings/Settings.ts b/log-viewer/src/features/settings/Settings.ts index b991f474..d3cd9bb4 100644 --- a/log-viewer/src/features/settings/Settings.ts +++ b/log-viewer/src/features/settings/Settings.ts @@ -8,23 +8,23 @@ export type LanaSettings = { timeline: { activeTheme: string; colors: { - 'Code Unit': '#88AE58'; - Workflow: '#51A16E'; - Method: '#2B8F81'; - Flow: '#5C8FA6'; - DML: '#B06868'; - SOQL: '#6D4C7D'; - 'System Method': '#8D6E63'; + Method: string; + 'Code Unit': string; + 'System Method': string; + Workflow: string; + DML: string; + SOQL: string; }; customThemes: { [key: string]: { + apex: string; codeUnit: string; - workflow: string; - method: string; - flow: string; + system: string; + automation: string; dml: string; soql: string; - system: string; + callout: string; + validation: string; }; }; legacy: boolean; diff --git a/log-viewer/src/features/timeline/__tests__/batching.test.ts b/log-viewer/src/features/timeline/__tests__/batching.test.ts index cae6d925..c2077375 100644 --- a/log-viewer/src/features/timeline/__tests__/batching.test.ts +++ b/log-viewer/src/features/timeline/__tests__/batching.test.ts @@ -12,7 +12,7 @@ * - Hierarchical rectangle collection */ -import type { LogEvent, LogSubCategory } from 'apex-log-parser'; +import type { LogCategory, LogEvent } from 'apex-log-parser'; import * as PIXI from 'pixi.js'; import { EventBatchRenderer } from '../optimised/EventBatchRenderer.js'; import { RectangleManager } from '../optimised/RectangleManager.js'; @@ -31,7 +31,7 @@ describe('EventBatchRenderer', () => { function createEvent( timestamp: number, duration: number, - subCategory: LogSubCategory, + category: LogCategory, children: LogEvent[] = [], ): LogEvent { return { @@ -41,11 +41,10 @@ describe('EventBatchRenderer', () => { total: duration, exclusive: duration, }, - subCategory, children, - text: `${subCategory} at ${timestamp}`, + text: `${category} at ${timestamp}`, lineNumber: 0, - category: subCategory, + category, } as unknown as LogEvent; } @@ -86,9 +85,9 @@ describe('EventBatchRenderer', () => { // Create batches for common categories batches = new Map([ [ - 'Method', + 'Apex', { - category: 'Method', + category: 'Apex', color: 0x88ae58, rectangles: [], isDirty: false, @@ -139,50 +138,50 @@ describe('EventBatchRenderer', () => { }); describe('batching by category', () => { - it('should group events by subCategory', () => { + it('should group events by category', () => { const events = [ - createEvent(0, 100, 'Method'), + createEvent(0, 100, 'Apex'), createEvent(200, 100, 'SOQL'), - createEvent(400, 100, 'Method'), + createEvent(400, 100, 'Apex'), ]; const viewport = createViewport(); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); const soqlBatch = batches.get('SOQL'); - expect(methodBatch?.rectangles).toHaveLength(2); + expect(apexBatch?.rectangles).toHaveLength(2); expect(soqlBatch?.rectangles).toHaveLength(1); }); it('should separate nested events into correct batches', () => { const child = createEvent(50, 20, 'SOQL'); - const parent = createEvent(0, 100, 'Method', [child]); + const parent = createEvent(0, 100, 'Apex', [child]); const events = [parent]; const viewport = createViewport(); setupAndRender(events, viewport); - const methodBatch = batches.get('Method')!; + const apexBatch = batches.get('Apex')!; const soqlBatch = batches.get('SOQL')!; - expect(methodBatch.rectangles).toHaveLength(1); + expect(apexBatch.rectangles).toHaveLength(1); expect(soqlBatch.rectangles).toHaveLength(1); }); it('should ignore events with unknown categories', () => { const events = [ - createEvent(0, 100, 'Method'), - createEvent(200, 100, 'UnknownCategory' as LogSubCategory), + createEvent(0, 100, 'Apex'), + createEvent(200, 100, 'UnknownCategory' as LogCategory), ]; const viewport = createViewport(); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); - expect(methodBatch?.rectangles).toHaveLength(1); + expect(apexBatch?.rectangles).toHaveLength(1); expect(batches.get('UnknownCategory')).toBeUndefined(); }); }); @@ -190,56 +189,56 @@ describe('EventBatchRenderer', () => { describe('horizontal culling (time-based)', () => { it('should render events within viewport time range', () => { const events = [ - createEvent(0, 100, 'Method'), - createEvent(200, 100, 'Method'), - createEvent(400, 100, 'Method'), + createEvent(0, 100, 'Apex'), + createEvent(200, 100, 'Apex'), + createEvent(400, 100, 'Apex'), ]; const viewport = createViewport(1, 0, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); - expect(methodBatch?.rectangles).toHaveLength(3); + const apexBatch = batches.get('Apex'); + expect(apexBatch?.rectangles).toHaveLength(3); }); it('should cull events before viewport', () => { - const events = [createEvent(0, 100, 'Method'), createEvent(200, 100, 'Method')]; + const events = [createEvent(0, 100, 'Apex'), createEvent(200, 100, 'Apex')]; const viewport = createViewport(1, 150, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); // First event should be culled, second should be visible - expect(methodBatch?.rectangles).toHaveLength(1); - expect(methodBatch?.rectangles[0]?.eventRef.timestamp).toBe(200); + expect(apexBatch?.rectangles).toHaveLength(1); + expect(apexBatch?.rectangles[0]?.eventRef.timestamp).toBe(200); }); it('should cull events after viewport', () => { const events = [ - createEvent(0, 100, 'Method'), - createEvent(1200, 100, 'Method'), // Starts after viewport end + createEvent(0, 100, 'Apex'), + createEvent(1200, 100, 'Apex'), // Starts after viewport end ]; const viewport = createViewport(1, 0, 0, 1000); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); // Second event should be culled - expect(methodBatch?.rectangles).toHaveLength(1); - expect(methodBatch?.rectangles[0]?.eventRef.timestamp).toBe(0); + expect(apexBatch?.rectangles).toHaveLength(1); + expect(apexBatch?.rectangles[0]?.eventRef.timestamp).toBe(0); }); it('should include partially visible events', () => { const events = [ - createEvent(50, 200, 'Method'), // Spans 50-250, viewport is 0-200 + createEvent(50, 200, 'Apex'), // Spans 50-250, viewport is 0-200 ]; const viewport = createViewport(1, 0, 0, 200); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); // Should be included even though only partially visible - expect(methodBatch?.rectangles).toHaveLength(1); + expect(apexBatch?.rectangles).toHaveLength(1); }); }); @@ -247,14 +246,14 @@ describe('EventBatchRenderer', () => { it('should render events within viewport depth range', () => { const level2 = createEvent(60, 10, 'DML'); const level1 = createEvent(50, 30, 'SOQL', [level2]); - const level0 = createEvent(0, 100, 'Method', [level1]); + const level0 = createEvent(0, 100, 'Apex', [level1]); const events = [level0]; const viewport = createViewport(); setupAndRender(events, viewport); // All three events should be visible - expect(batches.get('Method')?.rectangles).toHaveLength(1); + expect(batches.get('Apex')?.rectangles).toHaveLength(1); expect(batches.get('SOQL')?.rectangles).toHaveLength(1); expect(batches.get('DML')?.rectangles).toHaveLength(1); }); @@ -262,7 +261,7 @@ describe('EventBatchRenderer', () => { it('should cull events below viewport', () => { const level2 = createEvent(60, 10, 'DML'); const level1 = createEvent(50, 30, 'SOQL', [level2]); - const level0 = createEvent(0, 100, 'Method', [level1]); + const level0 = createEvent(0, 100, 'Apex', [level1]); const events = [level0]; // Pan down so only depth 2+ is visible @@ -271,13 +270,13 @@ describe('EventBatchRenderer', () => { setupAndRender(events, viewport); // Level 0 and 1 should be culled (depths < depthStart), level 2 should be visible - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); const soqlBatch = batches.get('SOQL'); const dmlBatch = batches.get('DML'); // With the new implementation, events are pre-computed, so only depth filtering applies // Depth 0 and 1 are culled, but depth 2 (DML) might still be visible - expect(methodBatch?.rectangles.length).toBeLessThanOrEqual(1); + expect(apexBatch?.rectangles.length).toBeLessThanOrEqual(1); expect(soqlBatch?.rectangles.length).toBeLessThanOrEqual(1); // Depth 2 might be visible since it's in the viewport expect(dmlBatch?.rectangles.length).toBeGreaterThanOrEqual(0); @@ -286,7 +285,7 @@ describe('EventBatchRenderer', () => { it('should cull events above viewport', () => { const level2 = createEvent(60, 10, 'DML'); const level1 = createEvent(50, 30, 'SOQL', [level2]); - const level0 = createEvent(0, 100, 'Method', [level1]); + const level0 = createEvent(0, 100, 'Apex', [level1]); const events = [level0]; // Viewport showing depths 0 and 1, but not 2 @@ -301,7 +300,7 @@ describe('EventBatchRenderer', () => { // But depth 2 is not (2 > 1) // However, since level 1 (SOQL) is visible and has children, // those children will be checked. Level 2 (DML) at depth 2 should be culled. - expect(batches.get('Method')?.rectangles).toHaveLength(1); + expect(batches.get('Apex')?.rectangles).toHaveLength(1); expect(batches.get('SOQL')?.rectangles).toHaveLength(1); expect(batches.get('DML')?.rectangles).toHaveLength(0); }); @@ -310,39 +309,39 @@ describe('EventBatchRenderer', () => { describe('minimum size filtering', () => { it('should cull events smaller than minimum size', () => { const events = [ - createEvent(0, 0.01, 'Method'), // Very small duration + createEvent(0, 0.01, 'Apex'), // Very small duration ]; // At zoom=1, event width = 0.01px (< MIN_RECT_SIZE = 0.05) const viewport = createViewport(1, 0, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); - expect(methodBatch?.rectangles).toHaveLength(0); + const apexBatch = batches.get('Apex'); + expect(apexBatch?.rectangles).toHaveLength(0); }); it('should render events that meet minimum size threshold', () => { const events = [ - createEvent(0, 3, 'Method'), // Width = 3px at zoom=1 (> MIN_RECT_SIZE = 2px) + createEvent(0, 3, 'Apex'), // Width = 3px at zoom=1 (> MIN_RECT_SIZE = 2px) ]; const viewport = createViewport(1, 0, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); - expect(methodBatch?.rectangles).toHaveLength(1); + const apexBatch = batches.get('Apex'); + expect(apexBatch?.rectangles).toHaveLength(1); }); it('should render small events when zoomed in', () => { const events = [ - createEvent(0, 1, 'Method'), // 1ns duration + createEvent(0, 1, 'Apex'), // 1ns duration ]; const viewport = createViewport(10, 0, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); - expect(methodBatch?.rectangles).toHaveLength(1); + const apexBatch = batches.get('Apex'); + expect(apexBatch?.rectangles).toHaveLength(1); }); }); @@ -350,13 +349,13 @@ describe('EventBatchRenderer', () => { it('should collect rectangles at correct depths', () => { const level2 = createEvent(60, 10, 'DML'); const level1 = createEvent(50, 30, 'SOQL', [level2]); - const level0 = createEvent(0, 100, 'Method', [level1]); + const level0 = createEvent(0, 100, 'Apex', [level1]); const events = [level0]; const viewport = createViewport(); setupAndRender(events, viewport); - const methodRect = batches.get('Method')?.rectangles[0]; + const methodRect = batches.get('Apex')?.rectangles[0]; const soqlRect = batches.get('SOQL')?.rectangles[0]; const dmlRect = batches.get('DML')?.rectangles[0]; @@ -369,7 +368,7 @@ describe('EventBatchRenderer', () => { it('should skip children if parent is not visible', () => { const child = createEvent(2000, 100, 'SOQL'); - const parent = createEvent(1500, 600, 'Method', [child]); + const parent = createEvent(1500, 600, 'Apex', [child]); const events = [parent]; // Viewport shows time 0-1000, parent starts at 1500 @@ -377,13 +376,13 @@ describe('EventBatchRenderer', () => { setupAndRender(events, viewport); // Both parent and child should be culled - expect(batches.get('Method')?.rectangles).toHaveLength(0); + expect(batches.get('Apex')?.rectangles).toHaveLength(0); expect(batches.get('SOQL')?.rectangles).toHaveLength(0); }); it('should process children even if parent is partially visible', () => { const child = createEvent(500, 100, 'SOQL'); - const parent = createEvent(400, 300, 'Method', [child]); + const parent = createEvent(400, 300, 'Apex', [child]); const events = [parent]; // Viewport shows time 0-600, parent extends to 700 @@ -391,20 +390,20 @@ describe('EventBatchRenderer', () => { setupAndRender(events, viewport); // Both should be visible - expect(batches.get('Method')?.rectangles).toHaveLength(1); + expect(batches.get('Apex')?.rectangles).toHaveLength(1); expect(batches.get('SOQL')?.rectangles).toHaveLength(1); }); }); describe('rectangle calculations', () => { it('should calculate correct rectangle positions with zoom', () => { - const events = [createEvent(100, 50, 'Method')]; + const events = [createEvent(100, 50, 'Apex')]; const viewport = createViewport(2, 0, 0); // 2x zoom setupAndRender(events, viewport); - const rect = batches.get('Method')?.rectangles[0]; + const rect = batches.get('Apex')?.rectangles[0]; // At 2x zoom: x = 100 * 2 = 200, width = 50 * 2 = 100 expect(rect?.x).toBe(200); @@ -412,24 +411,24 @@ describe('EventBatchRenderer', () => { }); it('should calculate correct rectangle height', () => { - const events = [createEvent(0, 100, 'Method')]; + const events = [createEvent(0, 100, 'Apex')]; const viewport = createViewport(); setupAndRender(events, viewport); - const rect = batches.get('Method')?.rectangles[0]; + const rect = batches.get('Apex')?.rectangles[0]; expect(rect?.height).toBe(TIMELINE_CONSTANTS.EVENT_HEIGHT); }); it('should preserve event reference in rectangle', () => { - const event = createEvent(0, 100, 'Method'); + const event = createEvent(0, 100, 'Apex'); const events = [event]; const viewport = createViewport(); setupAndRender(events, viewport); - const rect = batches.get('Method')?.rectangles[0]; + const rect = batches.get('Apex')?.rectangles[0]; expect(rect?.eventRef).toBe(event); }); @@ -437,28 +436,28 @@ describe('EventBatchRenderer', () => { describe('dirty flag management', () => { it('should mark batches as dirty during render', () => { - const events = [createEvent(0, 100, 'Method')]; + const events = [createEvent(0, 100, 'Apex')]; const viewport = createViewport(); setupAndRender(events, viewport); // After render, dirty flags should be cleared - expect(batches.get('Method')?.isDirty).toBe(false); + expect(batches.get('Apex')?.isDirty).toBe(false); }); it('should clear rectangles on each render', () => { - const events = [createEvent(0, 100, 'Method'), createEvent(200, 100, 'Method')]; + const events = [createEvent(0, 100, 'Apex'), createEvent(200, 100, 'Apex')]; const viewport = createViewport(); setupAndRender(events, viewport); - expect(batches.get('Method')?.rectangles).toHaveLength(2); + expect(batches.get('Apex')?.rectangles).toHaveLength(2); // Second render with different viewport (should recalculate) const viewport2 = createViewport(1, 150, 0); // Pan to cull first event const { visibleRects: visibleRects2, buckets: buckets2 } = rectangleManager.getCulledRectangles(viewport2); renderer.render(visibleRects2, buckets2); - expect(batches.get('Method')?.rectangles).toHaveLength(1); + expect(batches.get('Apex')?.rectangles).toHaveLength(1); }); }); @@ -477,10 +476,9 @@ describe('EventBatchRenderer', () => { it('should handle events without duration', () => { const event = { timestamp: 0, - subCategory: 'Method', + category: 'Apex', text: 'No duration', lineNumber: 0, - category: 'Method', children: [], duration: { total: 0, @@ -493,11 +491,11 @@ describe('EventBatchRenderer', () => { const viewport = createViewport(); setupAndRender(events, viewport); - expect(batches.get('Method')?.rectangles).toHaveLength(0); + expect(batches.get('Apex')?.rectangles).toHaveLength(0); }); it('should handle zero zoom gracefully', () => { - const events = [createEvent(0, 100, 'Method')]; + const events = [createEvent(0, 100, 'Apex')]; const viewport = createViewport(0, 0, 0); setupAndRender(events, viewport); @@ -525,17 +523,17 @@ describe('EventBatchRenderer', () => { // affect SearchHighlightRenderer's ability to render highlights for those events. const events = [ - createEvent(0, 100, 'Method'), // Will be culled when zoomed out + createEvent(0, 100, 'Apex'), // Will be culled when zoomed out ]; // Zoom out so screenWidth = 100 * 0.002 = 0.2px (< MIN_RECT_SIZE = 0.5px) const viewport = createViewport(0.002, 0, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); // Event should be culled by EventBatchRenderer (too small) - expect(methodBatch?.rectangles).toHaveLength(0); + expect(apexBatch?.rectangles).toHaveLength(0); // This confirms that EventBatchRenderer correctly culls small rectangles. // SearchHighlightRenderer is tested separately to ensure it enforces minimum @@ -543,16 +541,16 @@ describe('EventBatchRenderer', () => { }); it('should render events when they meet minimum size threshold after zoom', () => { - const events = [createEvent(0, 300, 'Method')]; + const events = [createEvent(0, 300, 'Apex')]; // Zoom in so screenWidth = 300 * 0.01 = 3px (> MIN_RECT_SIZE = 2px) const viewport = createViewport(0.01, 0, 0); setupAndRender(events, viewport); - const methodBatch = batches.get('Method'); + const apexBatch = batches.get('Apex'); // Event should be rendered (meets minimum size) - expect(methodBatch?.rectangles).toHaveLength(1); + expect(apexBatch?.rectangles).toHaveLength(1); }); }); }); diff --git a/log-viewer/src/features/timeline/__tests__/search-highlight.test.ts b/log-viewer/src/features/timeline/__tests__/search-highlight.test.ts index b473d2f3..3ca4fcc4 100644 --- a/log-viewer/src/features/timeline/__tests__/search-highlight.test.ts +++ b/log-viewer/src/features/timeline/__tests__/search-highlight.test.ts @@ -32,11 +32,10 @@ describe('SearchHighlightRenderer', () => { total: duration, exclusive: duration, }, - subCategory: 'Method', + category: 'Apex', children: [], text: 'Test Event', lineNumber: 0, - category: 'Method', } as unknown as LogEvent; return event; }; @@ -52,7 +51,7 @@ describe('SearchHighlightRenderer', () => { id: `${timestamp}-${depth}-0`, timestamp, duration, - type: 'Method', + type: 'Apex', text: 'Test Event', }; const rect: PrecomputedRect = { @@ -67,7 +66,7 @@ describe('SearchHighlightRenderer', () => { depth, duration, selfDuration: duration, - category: 'Method', + category: 'Apex', }; return { event: eventNode, diff --git a/log-viewer/src/features/timeline/__tests__/tooltip.test.ts b/log-viewer/src/features/timeline/__tests__/tooltip.test.ts index 8846f6ff..d5d8c926 100644 --- a/log-viewer/src/features/timeline/__tests__/tooltip.test.ts +++ b/log-viewer/src/features/timeline/__tests__/tooltip.test.ts @@ -30,7 +30,7 @@ describe('TimelineTooltipManager', () => { timestamp: number, duration: number, type: string = 'TestEvent', - subCategory: string = 'Method', + category: string = 'Apex', ): LogEvent { return { timestamp, @@ -41,10 +41,9 @@ describe('TimelineTooltipManager', () => { self: duration * 0.5, }, type, - subCategory, + category, text: `Event at ${timestamp}`, lineNumber: 42, - category: 'Method', children: [], isParent: true, // Required for tooltip to show dmlCount: { total: 0, self: 0 }, @@ -111,7 +110,7 @@ describe('TimelineTooltipManager', () => { tooltipManager.destroy(); tooltipManager = new TimelineTooltipManager(container, { - categoryColors: { Method: '#88ae58' }, + categoryColors: { Apex: '#88ae58' }, cursorOffset: 20, enableFlip: true, }); @@ -563,7 +562,7 @@ describe('TimelineTooltipManager', () => { it('should handle event with minimal data', () => { const event = { timestamp: 0, - category: 'Method', + category: 'Apex', children: [], isParent: true, text: 'Minimal event', diff --git a/log-viewer/src/features/timeline/components/TimelineView.ts b/log-viewer/src/features/timeline/components/TimelineView.ts index 9079d8c2..9f3c8c5b 100644 --- a/log-viewer/src/features/timeline/components/TimelineView.ts +++ b/log-viewer/src/features/timeline/components/TimelineView.ts @@ -24,13 +24,14 @@ import './TimelineSkeleton.js'; /* eslint-disable @typescript-eslint/naming-convention */ interface ThemeSettings { [key: string]: { + apex: string; codeUnit: string; - workflow: string; - method: string; - flow: string; + system: string; + automation: string; dml: string; soql: string; - system: string; + callout: string; + validation: string; }; } /* eslint-enable @typescript-eslint/naming-convention */ @@ -124,13 +125,14 @@ export class TimelineView extends LitElement { const themes: { [key: string]: TimelineColors } = {}; for (const [name, colors] of Object.entries(themeSettings)) { themes[name] = { + apex: colors.apex, codeUnit: colors.codeUnit, - workflow: colors.workflow, - method: colors.method, - flow: colors.flow, + system: colors.system, + automation: colors.automation, dml: colors.dml, soql: colors.soql, - system: colors.system, + callout: colors.callout, + validation: colors.validation, }; } return themes; @@ -139,20 +141,20 @@ export class TimelineView extends LitElement { private toTimelineKeys(colors: TimelineColors): TimelineGroup[] { return [ { - label: 'Code Unit', - fillColor: colors.codeUnit, + label: 'Apex', + fillColor: colors.apex, }, { - label: 'Workflow', - fillColor: colors.workflow, + label: 'Code Unit', + fillColor: colors.codeUnit, }, { - label: 'Method', - fillColor: colors.method, + label: 'System', + fillColor: colors.system, }, { - label: 'Flow', - fillColor: colors.flow, + label: 'Automation', + fillColor: colors.automation, }, { label: 'DML', @@ -163,9 +165,14 @@ export class TimelineView extends LitElement { fillColor: colors.soql, }, { - label: 'System Method', - fillColor: colors.system, + label: 'Callout', + fillColor: colors.callout, }, + //NOTE: add back once the parser is updated to include validation events + // { + // label: 'Validation', + // fillColor: colors.validation, + // }, ]; } } diff --git a/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts b/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts index a5b1ade8..e476b063 100644 --- a/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts +++ b/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts @@ -26,16 +26,17 @@ import { findEventByTimestamp } from '../../../core/utility/EventSearch.js'; import { formatDuration } from '../../../core/utility/Util.js'; import { goToRow } from '../../call-tree/components/CalltreeView.js'; import { getTheme } from '../themes/ThemeSelector.js'; -import type { - EventNode, - FindEventDetail, - FindResultsEventDetail, - HeatStripMetric, - HeatStripTimeSeries, - ModifierKeys, - TimelineMarker, - TimelineOptions, - ViewportState, +import { + BUCKET_CONSTANTS, + type EventNode, + type FindEventDetail, + type FindResultsEventDetail, + type HeatStripMetric, + type HeatStripTimeSeries, + type ModifierKeys, + type TimelineMarker, + type TimelineOptions, + type ViewportState, } from '../types/flamechart.types.js'; import type { SearchCursor } from '../types/search.types.js'; import { extractMarkers } from '../utils/marker-utils.js'; @@ -123,16 +124,8 @@ export class ApexLogTimeline { const markers = extractMarkers(this.apexLog); this.events = this.extractEvents(); - // Define categories for rectangle indexing (matches FlameChart batch categories) - const categories = new Set([ - 'Code Unit', - 'Workflow', - 'Method', - 'Flow', - 'DML', - 'SOQL', - 'System Method', - ]); + // Derive categories from shared constant (ensures compile-time sync with color map) + const categories = new Set(BUCKET_CONSTANTS.CATEGORY_PRIORITY); // Single-pass unified conversion: builds TreeNodes, navigation maps, // PrecomputedRects, maxDepth, and totalDuration in one O(n) traversal. @@ -255,7 +248,7 @@ export class ApexLogTimeline { id: `${result.event.timestamp}-${result.depth}`, timestamp: result.event.timestamp, duration: result.event.duration.total, - type: result.event.type ?? result.event.subCategory ?? 'UNKNOWN', + type: result.event.type ?? result.event.category ?? 'UNKNOWN', text: result.event.text, original: result.event, }; @@ -337,13 +330,14 @@ export class ApexLogTimeline { // Convert TimelineColors keys to the format expected by FlameChart /* eslint-disable @typescript-eslint/naming-convention */ return { + Apex: theme.apex, 'Code Unit': theme.codeUnit, - Workflow: theme.workflow, - Method: theme.method, - Flow: theme.flow, + System: theme.system, + Automation: theme.automation, DML: theme.dml, SOQL: theme.soql, - 'System Method': theme.system, + Callout: theme.callout, + Validation: theme.validation, }; /* eslint-enable @typescript-eslint/naming-convention */ } diff --git a/log-viewer/src/features/timeline/optimised/BucketColorResolver.ts b/log-viewer/src/features/timeline/optimised/BucketColorResolver.ts index 23afe787..d983cffe 100644 --- a/log-viewer/src/features/timeline/optimised/BucketColorResolver.ts +++ b/log-viewer/src/features/timeline/optimised/BucketColorResolver.ts @@ -8,7 +8,7 @@ * Resolves the display color for a pixel bucket based on category statistics. * Uses priority order with duration and count tie-breakers. * - * Priority order: DML > SOQL > Method > Code Unit > System Method > Flow > Workflow + * Priority order: DML > SOQL > Callout > Apex > Code Unit > System > Automation > Validation */ import type { CategoryStats } from '../types/flamechart.types.js'; @@ -22,13 +22,14 @@ import { BUCKET_CONSTANTS } from '../types/flamechart.types.js'; * All timeline code should use this via import or batchColors from theme. */ export const CATEGORY_COLORS: Record = { + Apex: 0x2b8f81, // #2B8F81 + 'Code Unit': 0x88ae58, // #88AE58 + System: 0x8d6e63, // #8D6E63 + Automation: 0x51a16e, // #51A16E DML: 0xb06868, // #B06868 SOQL: 0x6d4c7d, // #6D4C7D - Method: 0x2b8f81, // #2B8F81 - 'Code Unit': 0x88ae58, // #88AE58 - 'System Method': 0x8d6e63, // #8D6E63 - Flow: 0x5c8fa6, // #5C8FA6 - Workflow: 0x51a16e, // #51A16E + Callout: 0xcca033, // #CCA033 + Validation: 0x5c8fa6, // #5C8FA6 }; /** diff --git a/log-viewer/src/features/timeline/optimised/RectangleManager.ts b/log-viewer/src/features/timeline/optimised/RectangleManager.ts index 8d2c1f93..2b908752 100644 --- a/log-viewer/src/features/timeline/optimised/RectangleManager.ts +++ b/log-viewer/src/features/timeline/optimised/RectangleManager.ts @@ -293,11 +293,11 @@ export class RectangleManager { const len = currentEvents.length; for (let i = 0; i < len; i++) { const event = currentEvents[i]!; - const { duration, subCategory, timestamp, exitStamp, children } = event; + const { duration, category, timestamp, exitStamp, children } = event; // Check if this event should be rendered - if (duration.total && subCategory) { - const rects = this.rectsByCategory.get(subCategory); + if (duration.total && category) { + const rects = this.rectsByCategory.get(category); if (rects) { // Create persistent rect object (updated during culling) // ID format: timestamp-depth-childIndex @@ -308,7 +308,7 @@ export class RectangleManager { depth, duration: duration.total, selfDuration: duration.self, - category: subCategory, + category, eventRef: event, x: 0, y: depthY, diff --git a/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts b/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts index 422739d8..ba68e175 100644 --- a/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts +++ b/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts @@ -348,7 +348,7 @@ export class TimelineTooltipManager { '', event.text + (event.suffix ?? ''), rows, - this.options.categoryColors[event.subCategory] || '', + this.options.categoryColors[event.category] || '', ); } diff --git a/log-viewer/src/features/timeline/optimised/__tests__/BucketColorResolver.test.ts b/log-viewer/src/features/timeline/optimised/__tests__/BucketColorResolver.test.ts index 8732e832..1200bc63 100644 --- a/log-viewer/src/features/timeline/optimised/__tests__/BucketColorResolver.test.ts +++ b/log-viewer/src/features/timeline/optimised/__tests__/BucketColorResolver.test.ts @@ -8,7 +8,7 @@ import { resolveColor } from '../BucketColorResolver.js'; /** * Tests for BucketColorResolver - resolves bucket color from category statistics. * - * Priority order: DML > SOQL > Method > Code Unit > System Method > Flow > Workflow + * Priority order: DML > SOQL > Callout > Apex > Code Unit > System > Automation > Validation * Tie-breakers: total duration → event count */ @@ -32,7 +32,7 @@ describe('BucketColorResolver', () => { const stats = createCategoryStats({ DML: { count: 1, totalDuration: 100 }, SOQL: { count: 10, totalDuration: 1000 }, - Method: { count: 100, totalDuration: 10000 }, + Apex: { count: 100, totalDuration: 10000 }, }); const result = resolveColor(stats); @@ -42,10 +42,10 @@ describe('BucketColorResolver', () => { expect(result.dominantCategory).toBe('DML'); }); - it('should prioritize SOQL over Method, Code Unit, etc.', () => { + it('should prioritize SOQL over Apex, Code Unit, etc.', () => { const stats = createCategoryStats({ SOQL: { count: 1, totalDuration: 100 }, - Method: { count: 10, totalDuration: 1000 }, + Apex: { count: 10, totalDuration: 1000 }, 'Code Unit': { count: 100, totalDuration: 10000 }, }); @@ -56,91 +56,86 @@ describe('BucketColorResolver', () => { expect(result.dominantCategory).toBe('SOQL'); }); - it('should prioritize Method over Code Unit, System Method, Flow, Workflow', () => { + it('should prioritize Callout over Apex, Code Unit, System', () => { const stats = createCategoryStats({ - Method: { count: 1, totalDuration: 100 }, - 'Code Unit': { count: 10, totalDuration: 1000 }, - 'System Method': { count: 100, totalDuration: 10000 }, + Callout: { count: 1, totalDuration: 100 }, + Apex: { count: 10, totalDuration: 1000 }, + System: { count: 100, totalDuration: 10000 }, }); const result = resolveColor(stats); - // Method color is #2B8F81 = 0x2B8F81 - expect(result.color).toBe(0x2b8f81); - expect(result.dominantCategory).toBe('Method'); + // Callout color is #CCA033 = 0xCCA033 + expect(result.color).toBe(0xcca033); + expect(result.dominantCategory).toBe('Callout'); }); - it('should prioritize Code Unit over System Method, Flow, Workflow', () => { + it('should prioritize Apex over Code Unit, System, Automation', () => { const stats = createCategoryStats({ - 'Code Unit': { count: 1, totalDuration: 100 }, - 'System Method': { count: 10, totalDuration: 1000 }, - Flow: { count: 100, totalDuration: 10000 }, + Apex: { count: 1, totalDuration: 100 }, + 'Code Unit': { count: 10, totalDuration: 1000 }, + System: { count: 100, totalDuration: 10000 }, }); const result = resolveColor(stats); - // Code Unit color is #88AE58 = 0x88AE58 - expect(result.color).toBe(0x88ae58); - expect(result.dominantCategory).toBe('Code Unit'); + // Apex color is #2B8F81 = 0x2B8F81 + expect(result.color).toBe(0x2b8f81); + expect(result.dominantCategory).toBe('Apex'); }); - it('should prioritize System Method over Flow and Workflow', () => { + it('should prioritize System over Automation', () => { const stats = createCategoryStats({ - 'System Method': { count: 1, totalDuration: 100 }, - Flow: { count: 10, totalDuration: 1000 }, - Workflow: { count: 100, totalDuration: 10000 }, + System: { count: 1, totalDuration: 100 }, + Automation: { count: 10, totalDuration: 1000 }, }); const result = resolveColor(stats); - // System Method color is #8D6E63 = 0x8D6E63 + // System color is #8D6E63 = 0x8D6E63 expect(result.color).toBe(0x8d6e63); - expect(result.dominantCategory).toBe('System Method'); + expect(result.dominantCategory).toBe('System'); }); - it('should prioritize Flow over Workflow', () => { + it('should prioritize Automation over Validation', () => { const stats = createCategoryStats({ - Flow: { count: 1, totalDuration: 100 }, - Workflow: { count: 10, totalDuration: 1000 }, + Automation: { count: 1, totalDuration: 100 }, + Validation: { count: 10, totalDuration: 1000 }, }); const result = resolveColor(stats); - // Flow color is #5C8FA6 = 0x5C8FA6 - expect(result.color).toBe(0x5c8fa6); - expect(result.dominantCategory).toBe('Flow'); + // Automation color is #51A16E = 0x51A16E + expect(result.color).toBe(0x51a16e); + expect(result.dominantCategory).toBe('Automation'); }); - it('should return Workflow color when only Workflow present', () => { + it('should return Validation color when only Validation present', () => { const stats = createCategoryStats({ - Workflow: { count: 5, totalDuration: 500 }, + Validation: { count: 5, totalDuration: 500 }, }); const result = resolveColor(stats); - // Workflow color is #51A16E = 0x51A16E - expect(result.color).toBe(0x51a16e); - expect(result.dominantCategory).toBe('Workflow'); + // Validation color is #5C8FA6 = 0x5C8FA6 + expect(result.color).toBe(0x5c8fa6); + expect(result.dominantCategory).toBe('Validation'); }); }); describe('duration tie-breaking', () => { it('should use duration to break ties between same-priority categories', () => { - // Method and Method have same priority (both are "Method") - // But we can test with a hypothetical case or use different duration values - // within same category - this test verifies the algorithm const stats = createCategoryStats({ - Method: { count: 5, totalDuration: 1000 }, // Higher duration + Apex: { count: 5, totalDuration: 1000 }, }); const result = resolveColor(stats); - expect(result.dominantCategory).toBe('Method'); + expect(result.dominantCategory).toBe('Apex'); }); }); describe('count tie-breaking', () => { it('should use count when priority and duration are equal', () => { - // Single category - count doesn't matter for single item const stats = createCategoryStats({ DML: { count: 10, totalDuration: 500 }, }); @@ -166,14 +161,14 @@ describe('BucketColorResolver', () => { it('should prioritize known categories over unknown ones', () => { const stats = createCategoryStats({ UnknownCategory: { count: 100, totalDuration: 10000 }, - Workflow: { count: 1, totalDuration: 100 }, + Automation: { count: 1, totalDuration: 100 }, }); const result = resolveColor(stats); - // Workflow is known, so it should win over unknown + // Automation is known, so it should win over unknown expect(result.color).toBe(0x51a16e); - expect(result.dominantCategory).toBe('Workflow'); + expect(result.dominantCategory).toBe('Automation'); }); }); diff --git a/log-viewer/src/features/timeline/optimised/__tests__/RectangleManager.bucket.test.ts b/log-viewer/src/features/timeline/optimised/__tests__/RectangleManager.bucket.test.ts index c1387342..a583450a 100644 --- a/log-viewer/src/features/timeline/optimised/__tests__/RectangleManager.bucket.test.ts +++ b/log-viewer/src/features/timeline/optimised/__tests__/RectangleManager.bucket.test.ts @@ -2,7 +2,7 @@ * Copyright (c) 2025 Certinia Inc. All rights reserved. */ -import type { LogEvent, LogSubCategory } from 'apex-log-parser'; +import type { LogCategory, LogEvent } from 'apex-log-parser'; import type { ViewportState } from '../../types/flamechart.types.js'; import { TIMELINE_CONSTANTS } from '../../types/flamechart.types.js'; import { legacyCullRectangles } from '../LegacyViewportCuller.js'; @@ -23,14 +23,14 @@ import { RectangleManager } from '../RectangleManager.js'; function createEvent( timestamp: number, duration: number, - category: LogSubCategory, + category: LogCategory, children?: LogEvent[], ): LogEvent { return { timestamp, exitStamp: timestamp + duration, duration: { total: duration, self: duration, netSelf: duration }, - subCategory: category, + category, type: 'METHOD_ENTRY', text: `Event at ${timestamp}`, children: children ?? [], @@ -84,23 +84,24 @@ function countBuckets(bucketsMap: Map): number { describe('Legacy bucket aggregation', () => { const categories = new Set([ - 'Method', - 'SOQL', - 'DML', + 'Apex', 'Code Unit', - 'System Method', - 'Flow', - 'Workflow', + 'System', + 'Automation', + 'DML', + 'SOQL', + 'Callout', + 'Validation', ]); describe('event size classification', () => { it('should return events > 2px in visibleRects', () => { // Event with duration 3ns at zoom=1 gives 3px width (> MIN_RECT_SIZE) - const events = [createEvent(0, 3, 'Method')]; + const events = [createEvent(0, 3, 'Apex')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); - expect(result.visibleRects.get('Method')).toHaveLength(1); + expect(result.visibleRects.get('Apex')).toHaveLength(1); expect(countBuckets(result.buckets)).toBe(0); expect(result.stats.visibleCount).toBe(1); expect(result.stats.bucketedEventCount).toBe(0); @@ -108,12 +109,12 @@ describe('Legacy bucket aggregation', () => { it('should aggregate events <= 2px into buckets', () => { // Event with duration 1ns at zoom=1 gives 1px width (<= MIN_RECT_SIZE) - const events = [createEvent(0, 1, 'Method')]; + const events = [createEvent(0, 1, 'Apex')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); // No visible rects (event is too small), so category has no entry - expect(result.visibleRects.has('Method')).toBe(false); + expect(result.visibleRects.has('Apex')).toBe(false); expect(countBuckets(result.buckets)).toBe(1); expect(result.stats.visibleCount).toBe(0); expect(result.stats.bucketedEventCount).toBe(1); @@ -121,15 +122,15 @@ describe('Legacy bucket aggregation', () => { it('should correctly separate visible and bucketed events', () => { const events = [ - createEvent(0, 5, 'Method'), // 5px at zoom=1 - visible - createEvent(10, 1, 'Method'), // 1px at zoom=1 - bucketed + createEvent(0, 5, 'Apex'), // 5px at zoom=1 - visible + createEvent(10, 1, 'Apex'), // 1px at zoom=1 - bucketed createEvent(20, 3, 'SOQL'), // 3px at zoom=1 - visible createEvent(30, 0.5, 'SOQL'), // 0.5px at zoom=1 - bucketed ]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); - expect(result.visibleRects.get('Method')).toHaveLength(1); + expect(result.visibleRects.get('Apex')).toHaveLength(1); expect(result.visibleRects.get('SOQL')).toHaveLength(1); expect(result.stats.visibleCount).toBe(2); expect(result.stats.bucketedEventCount).toBe(2); @@ -139,7 +140,7 @@ describe('Legacy bucket aggregation', () => { describe('bucket time alignment', () => { it('should create time-aligned bucket boundaries', () => { // At zoom=1, bucket width is 2ns (2px / 1) - const events = [createEvent(5, 1, 'Method')]; // Event at timestamp 5 + const events = [createEvent(5, 1, 'Apex')]; // Event at timestamp 5 const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -155,7 +156,7 @@ describe('Legacy bucket aggregation', () => { it('should group events in same time bucket together', () => { // Two events at timestamps 4 and 5 should be in same bucket (index 2, range [4,6)) - const events = [createEvent(4, 1, 'Method'), createEvent(5, 1, 'Method')]; + const events = [createEvent(4, 1, 'Apex'), createEvent(5, 1, 'Apex')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -168,8 +169,8 @@ describe('Legacy bucket aggregation', () => { it('should create separate buckets for different time ranges', () => { // Events at timestamps 0 and 10 should be in different buckets const events = [ - createEvent(0, 1, 'Method'), // bucket index 0 - createEvent(10, 1, 'Method'), // bucket index 5 + createEvent(0, 1, 'Apex'), // bucket index 0 + createEvent(10, 1, 'Apex'), // bucket index 5 ]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -182,7 +183,7 @@ describe('Legacy bucket aggregation', () => { it('should create separate buckets per depth level', () => { // Parent and child at same time but different depths const child = createEvent(0, 1, 'SOQL'); - const parent = createEvent(0, 1, 'Method', [child]); + const parent = createEvent(0, 1, 'Apex', [child]); const events = [parent]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -196,7 +197,7 @@ describe('Legacy bucket aggregation', () => { }); it('should set correct Y position based on depth', () => { - const events = [createEvent(0, 1, 'Method')]; + const events = [createEvent(0, 1, 'Apex')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -205,7 +206,7 @@ describe('Legacy bucket aggregation', () => { // Test depth 1 const child = createEvent(0, 1, 'SOQL'); - const parent = createEvent(0, 2, 'Method', [child]); // parent is visible + const parent = createEvent(0, 2, 'Apex', [child]); // parent is visible const events2 = [parent]; // At zoom=0.5, parent (2ns) becomes 1px (bucketed), child (1ns) becomes 0.5px (bucketed) @@ -221,9 +222,9 @@ describe('Legacy bucket aggregation', () => { describe('bucket category statistics', () => { it('should track event counts per category', () => { const events = [ - createEvent(0, 1, 'Method'), + createEvent(0, 1, 'Apex'), createEvent(1, 1, 'SOQL'), - createEvent(0.5, 1, 'Method'), + createEvent(0.5, 1, 'Apex'), ]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -234,24 +235,24 @@ describe('Legacy bucket aggregation', () => { expect(allBuckets.length).toBeGreaterThanOrEqual(1); const bucket = allBuckets[0]!; - expect(bucket.categoryStats.byCategory.get('Method')?.count).toBe(2); + expect(bucket.categoryStats.byCategory.get('Apex')?.count).toBe(2); expect(bucket.categoryStats.byCategory.get('SOQL')?.count).toBe(1); }); it('should track total duration per category', () => { - const events = [createEvent(0, 1, 'Method'), createEvent(0.5, 0.5, 'Method')]; + const events = [createEvent(0, 1, 'Apex'), createEvent(0.5, 0.5, 'Apex')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); const allBuckets = getAllBuckets(result.buckets); const bucket = allBuckets[0]!; - expect(bucket.categoryStats.byCategory.get('Method')?.totalDuration).toBe(1.5); + expect(bucket.categoryStats.byCategory.get('Apex')?.totalDuration).toBe(1.5); }); }); describe('bucket color resolution', () => { it('should prioritize DML over Method in mixed bucket', () => { - const events = [createEvent(0, 1, 'Method'), createEvent(0.5, 1, 'DML')]; + const events = [createEvent(0, 1, 'Apex'), createEvent(0.5, 1, 'DML')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -264,7 +265,7 @@ describe('Legacy bucket aggregation', () => { }); it('should prioritize SOQL over Method in mixed bucket', () => { - const events = [createEvent(0, 1, 'Method'), createEvent(0.5, 1, 'SOQL')]; + const events = [createEvent(0, 1, 'Apex'), createEvent(0.5, 1, 'SOQL')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -276,7 +277,7 @@ describe('Legacy bucket aggregation', () => { describe('bucket color blending', () => { it('should have a valid color for single event', () => { - const events = [createEvent(0, 1, 'Method')]; + const events = [createEvent(0, 1, 'Apex')]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -291,7 +292,7 @@ describe('Legacy bucket aggregation', () => { // events appear dimmer than child events. All buckets now render at full color. // Create a bucket with a single event - const singleEvent = [createEvent(0, 1, 'Method')]; + const singleEvent = [createEvent(0, 1, 'Apex')]; const singleViewport = createViewport(1, 0, 0); const singleResult = cullRectanglesLegacy(singleEvent, categories, singleViewport); const singleBuckets = getAllBuckets(singleResult.buckets); @@ -300,7 +301,7 @@ describe('Legacy bucket aggregation', () => { // Create many events in same bucket const manyEvents: LogEvent[] = []; for (let i = 0; i < 50; i++) { - manyEvents.push(createEvent(i * 0.03, 0.01, 'Method')); // All in bucket index 0 + manyEvents.push(createEvent(i * 0.03, 0.01, 'Apex')); // All in bucket index 0 } const manyViewport = createViewport(1, 0, 0); const manyResult = cullRectanglesLegacy(manyEvents, categories, manyViewport); @@ -314,8 +315,8 @@ describe('Legacy bucket aggregation', () => { describe('bucket event references', () => { it('should store references to all aggregated events', () => { - const event1 = createEvent(0, 1, 'Method'); - const event2 = createEvent(0.5, 1, 'Method'); + const event1 = createEvent(0, 1, 'Apex'); + const event2 = createEvent(0.5, 1, 'Apex'); const events = [event1, event2]; const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); @@ -330,8 +331,8 @@ describe('Legacy bucket aggregation', () => { describe('render statistics', () => { it('should track correct stats', () => { const events = [ - createEvent(0, 5, 'Method'), // visible - createEvent(10, 1, 'Method'), // bucketed + createEvent(0, 5, 'Apex'), // visible + createEvent(10, 1, 'Apex'), // bucketed createEvent(20, 1, 'SOQL'), // bucketed ]; const viewport = createViewport(1, 0, 0); @@ -346,7 +347,7 @@ describe('Legacy bucket aggregation', () => { // Create 5 events in same bucket const events: LogEvent[] = []; for (let i = 0; i < 5; i++) { - events.push(createEvent(i * 0.3, 0.1, 'Method')); + events.push(createEvent(i * 0.3, 0.1, 'Apex')); } const viewport = createViewport(1, 0, 0); const result = cullRectanglesLegacy(events, categories, viewport); diff --git a/log-viewer/src/features/timeline/optimised/__tests__/TemporalSegmentTree.test.ts b/log-viewer/src/features/timeline/optimised/__tests__/TemporalSegmentTree.test.ts index 36bcc40b..5a56799f 100644 --- a/log-viewer/src/features/timeline/optimised/__tests__/TemporalSegmentTree.test.ts +++ b/log-viewer/src/features/timeline/optimised/__tests__/TemporalSegmentTree.test.ts @@ -2,7 +2,7 @@ * Copyright (c) 2025 Certinia Inc. All rights reserved. */ -import type { LogEvent, LogSubCategory } from 'apex-log-parser'; +import type { LogCategory, LogEvent } from 'apex-log-parser'; import type { PixelBucket, ViewportState } from '../../types/flamechart.types.js'; import { TIMELINE_CONSTANTS } from '../../types/flamechart.types.js'; import { legacyCullRectangles } from '../LegacyViewportCuller.js'; @@ -20,14 +20,14 @@ import { TemporalSegmentTree } from '../TemporalSegmentTree.js'; function createEvent( timestamp: number, duration: number, - category: LogSubCategory, + category: LogCategory, children?: LogEvent[], ): LogEvent { return { timestamp, exitStamp: timestamp + duration, duration: { total: duration, self: duration, netSelf: duration }, - subCategory: category, + category, type: 'METHOD_ENTRY', text: `Event at ${timestamp}`, children: children ?? [], @@ -71,19 +71,20 @@ function countBuckets(bucketsMap: Map): number { describe('TemporalSegmentTree', () => { const categories = new Set([ - 'Method', - 'SOQL', - 'DML', + 'Apex', 'Code Unit', - 'System Method', - 'Flow', - 'Workflow', + 'System', + 'Automation', + 'DML', + 'SOQL', + 'Callout', + 'Validation', ]); describe('tree building', () => { it('should build tree from rectangles', () => { const events = [ - createEvent(0, 10, 'Method'), + createEvent(0, 10, 'Apex'), createEvent(20, 10, 'SOQL'), createEvent(40, 10, 'DML'), ]; @@ -95,7 +96,7 @@ describe('TemporalSegmentTree', () => { it('should handle events at multiple depths', () => { const events = [ - createEvent(0, 100, 'Method', [createEvent(10, 30, 'SOQL'), createEvent(50, 30, 'DML')]), + createEvent(0, 100, 'Apex', [createEvent(10, 30, 'SOQL'), createEvent(50, 30, 'DML')]), ]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -114,21 +115,21 @@ describe('TemporalSegmentTree', () => { describe('query - display density principle', () => { it('should return visible rects for events > threshold', () => { // Event with duration 10ns at zoom=1 gives 10px width (> 2px threshold) - const events = [createEvent(0, 10, 'Method')]; + const events = [createEvent(0, 10, 'Apex')]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); const viewport = createViewport(1, 0, 0); const result = tree.query(viewport); - expect(result.visibleRects.get('Method')).toHaveLength(1); + expect(result.visibleRects.get('Apex')).toHaveLength(1); expect(countBuckets(result.buckets)).toBe(0); expect(result.stats.visibleCount).toBe(1); }); it('should return buckets for events <= threshold', () => { // Event with duration 1ns at zoom=1 gives 1px width (<= 2px threshold) - const events = [createEvent(0, 1, 'Method')]; + const events = [createEvent(0, 1, 'Apex')]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -136,7 +137,7 @@ describe('TemporalSegmentTree', () => { const result = tree.query(viewport); // Pre-initialized map has empty arrays for known categories - expect(result.visibleRects.get('Method')).toHaveLength(0); + expect(result.visibleRects.get('Apex')).toHaveLength(0); expect(countBuckets(result.buckets)).toBe(1); expect(result.stats.bucketedEventCount).toBe(1); }); @@ -144,7 +145,7 @@ describe('TemporalSegmentTree', () => { it('should aggregate multiple small events into buckets', () => { // Multiple small events at zoom=0.1 (threshold = 20ns) const events = [ - createEvent(0, 5, 'Method'), + createEvent(0, 5, 'Apex'), createEvent(10, 5, 'SOQL'), createEvent(20, 5, 'DML'), ]; @@ -162,10 +163,10 @@ describe('TemporalSegmentTree', () => { describe('zoom level transitions', () => { it('should return more detail when zoomed in', () => { const events = [ - createEvent(0, 5, 'Method'), + createEvent(0, 5, 'Apex'), createEvent(10, 5, 'SOQL'), createEvent(20, 5, 'DML'), - createEvent(30, 5, 'Method'), + createEvent(30, 5, 'Apex'), ]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -184,7 +185,7 @@ describe('TemporalSegmentTree', () => { it('should maintain total event count across zoom levels', () => { const events = [ - createEvent(0, 5, 'Method'), + createEvent(0, 5, 'Apex'), createEvent(10, 5, 'SOQL'), createEvent(20, 5, 'DML'), ]; @@ -213,7 +214,7 @@ describe('TemporalSegmentTree', () => { describe('viewport culling', () => { it('should exclude events outside time bounds', () => { const events = [ - createEvent(0, 10, 'Method'), + createEvent(0, 10, 'Apex'), createEvent(100, 10, 'SOQL'), createEvent(200, 10, 'DML'), ]; @@ -231,7 +232,7 @@ describe('TemporalSegmentTree', () => { it('should exclude events outside depth bounds', () => { const events = [ - createEvent(0, 100, 'Method', [createEvent(10, 80, 'SOQL', [createEvent(20, 60, 'DML')])]), + createEvent(0, 100, 'Apex', [createEvent(10, 80, 'SOQL', [createEvent(20, 60, 'DML')])]), ]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -286,9 +287,9 @@ describe('TemporalSegmentTree', () => { it('should include event count in bucket', () => { // Multiple events that will be aggregated const events = [ - createEvent(0, 1, 'Method'), - createEvent(1, 1, 'Method'), - createEvent(2, 1, 'Method'), + createEvent(0, 1, 'Apex'), + createEvent(1, 1, 'Apex'), + createEvent(2, 1, 'Apex'), ]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -301,7 +302,7 @@ describe('TemporalSegmentTree', () => { }); it('should include category stats for tooltips', () => { - const events = [createEvent(0, 1, 'Method'), createEvent(1, 1, 'SOQL')]; + const events = [createEvent(0, 1, 'Apex'), createEvent(1, 1, 'SOQL')]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -319,10 +320,10 @@ describe('TemporalSegmentTree', () => { describe('integration with RectangleManager', () => { it('should produce same event count as legacy implementation', () => { const events = [ - createEvent(0, 10, 'Method'), + createEvent(0, 10, 'Apex'), createEvent(20, 5, 'SOQL'), createEvent(30, 3, 'DML'), - createEvent(40, 1, 'Method'), + createEvent(40, 1, 'Apex'), ]; // Both implementations now use segment tree (legacy is in LegacyViewportCuller) @@ -350,7 +351,7 @@ describe('TemporalSegmentTree', () => { // Child B: timeStart=50, timeEnd=60 (short event, ends before A) // Query for time range [70, 90] should return Child A const events = [ - createEvent(0, 100, 'Method'), // timeStart=0, timeEnd=100 + createEvent(0, 100, 'Apex'), // timeStart=0, timeEnd=100 createEvent(50, 10, 'SOQL'), // timeStart=50, timeEnd=60 ]; const manager = new RectangleManager(events, categories); @@ -366,16 +367,16 @@ describe('TemporalSegmentTree', () => { // and overlaps with query range [70, 90] const totalEvents = result.stats.visibleCount + result.stats.bucketedEventCount; expect(totalEvents).toBe(1); - expect(result.visibleRects.get('Method')?.length).toBe(1); + expect(result.visibleRects.get('Apex')?.length).toBe(1); }); it('should correctly compute branch timeEnd as max of all children', () => { // Multiple events with varying durations to test max computation const events = [ - createEvent(0, 50, 'Method'), // timeEnd=50 + createEvent(0, 50, 'Apex'), // timeEnd=50 createEvent(10, 100, 'SOQL'), // timeEnd=110 (longest) createEvent(20, 30, 'DML'), // timeEnd=50 - createEvent(30, 20, 'Method'), // timeEnd=50 + createEvent(30, 20, 'Apex'), // timeEnd=50 ]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -392,7 +393,7 @@ describe('TemporalSegmentTree', () => { it('should find events via queryEventsInRegion with correct branch bounds', () => { // Same scenario but using queryEventsInRegion for hit testing const events = [ - createEvent(0, 100, 'Method'), // timeStart=0, timeEnd=100 + createEvent(0, 100, 'Apex'), // timeStart=0, timeEnd=100 createEvent(50, 10, 'SOQL'), // timeStart=50, timeEnd=60 ]; const manager = new RectangleManager(events, categories); @@ -402,14 +403,14 @@ describe('TemporalSegmentTree', () => { const eventsInRegion = tree.queryEventsInRegion(70, 90, 0, 0); expect(eventsInRegion).toHaveLength(1); - expect(eventsInRegion[0]?.subCategory).toBe('Method'); + expect(eventsInRegion[0]?.category).toBe('Apex'); }); }); describe('fill ratio and brightness', () => { it('should calculate fill ratio for buckets', () => { // Single short event in a wider time span = low fill ratio - const events = [createEvent(0, 1, 'Method')]; + const events = [createEvent(0, 1, 'Apex')]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); @@ -424,7 +425,7 @@ describe('TemporalSegmentTree', () => { it('should use full brightness for single-event buckets (optimization)', () => { // Single event bucket should have full brightness (eventCount === 1) - const events = [createEvent(0, 1, 'Method')]; + const events = [createEvent(0, 1, 'Apex')]; const manager = new RectangleManager(events, categories); const tree = new TemporalSegmentTree(manager.getRectsByCategory()); diff --git a/log-viewer/src/features/timeline/optimised/interaction/HitTestManager.ts b/log-viewer/src/features/timeline/optimised/interaction/HitTestManager.ts index 85dadd26..1de22805 100644 --- a/log-viewer/src/features/timeline/optimised/interaction/HitTestManager.ts +++ b/log-viewer/src/features/timeline/optimised/interaction/HitTestManager.ts @@ -165,7 +165,7 @@ export class HitTestManager { id: `${logEvent.timestamp}-${depth}`, timestamp: logEvent.timestamp, duration: logEvent.duration?.total ?? 0, - type: logEvent.type ?? logEvent.subCategory ?? 'UNKNOWN', + type: logEvent.type ?? logEvent.category ?? 'UNKNOWN', text: logEvent.text, original: logEvent, }; @@ -288,7 +288,7 @@ export class HitTestManager { let bestDuration = -1; for (const event of events) { - const priority = priorityMap.get(event.subCategory) ?? Infinity; + const priority = priorityMap.get(event.category) ?? Infinity; const duration = event.duration?.total ?? 0; // Priority wins first (lower index = higher priority) diff --git a/log-viewer/src/features/timeline/optimised/minimap/MinimapDensityQuery.ts b/log-viewer/src/features/timeline/optimised/minimap/MinimapDensityQuery.ts index 4b6b2307..06259b9d 100644 --- a/log-viewer/src/features/timeline/optimised/minimap/MinimapDensityQuery.ts +++ b/log-viewer/src/features/timeline/optimised/minimap/MinimapDensityQuery.ts @@ -28,13 +28,14 @@ * score[category] = onTopTime[category] × CATEGORY_WEIGHTS[category] * winner = argmax(score) * - * Example: SOQL at depth 2 covers 0-100ms with Method child at depth 3 covering 30-80ms + * Example: SOQL at depth 2 covers 0-100ms with Apex child at depth 3 covering 30-80ms * - SOQL is on-top at 0-30ms and 80-100ms = 50ms total (50%) - * - Method is on-top at 30-80ms = 50ms total (50%) - * - With weights: SOQL = 50% × 2.5 = 125, Method = 50% × 1.0 = 50 + * - Apex is on-top at 30-80ms = 50ms total (50%) + * - With weights: SOQL = 50% × 2.5 = 125, Apex = 50% × 1.0 = 50 * - SOQL wins because its weighted score is higher */ +import type { BucketCategoryPriority } from '../../types/flamechart.types.js'; import type { PrecomputedRect } from '../RectangleManager.js'; import type { SkylineFrame, TemporalSegmentTree } from '../TemporalSegmentTree.js'; @@ -87,14 +88,15 @@ export interface MinimapDensityData { * Balance: DML at 2.5x means it can win over a Method child 1-2 levels deeper, * but a child 5+ levels deeper will still dominate (depth² wins at larger gaps). */ -const CATEGORY_WEIGHTS: Record = { +const CATEGORY_WEIGHTS: Partial> = { DML: 2.5, SOQL: 2.5, - Method: 1.0, + Callout: 1.5, + Apex: 1.0, 'Code Unit': 1.0, - 'System Method': 0.8, - Flow: 0.8, - Workflow: 0.8, + System: 0.8, + Automation: 0.8, + Validation: 0.8, }; /** @@ -526,7 +528,7 @@ export class MinimapDensityQuery { ): string { // Fast path: no frames if (frames.length === 0) { - return 'Method'; + return 'Apex'; } // Fast path: single frame @@ -612,11 +614,11 @@ export class MinimapDensityQuery { } // Apply category weights and find winner - let winningCategory = 'Method'; + let winningCategory = 'Apex'; let winningScore = -1; for (const [category, time] of onTopTime) { - const weight = CATEGORY_WEIGHTS[category] ?? 1.0; + const weight = CATEGORY_WEIGHTS[category as BucketCategoryPriority] ?? 1.0; const score = time * weight; if (score > winningScore) { winningScore = score; diff --git a/log-viewer/src/features/timeline/services/Timeline.ts b/log-viewer/src/features/timeline/services/Timeline.ts index cc955272..e23c8bab 100644 --- a/log-viewer/src/features/timeline/services/Timeline.ts +++ b/log-viewer/src/features/timeline/services/Timeline.ts @@ -2,7 +2,7 @@ * Copyright (c) 2020 Certinia Inc. All rights reserved. */ //TODO:Refactor - usage should look more like `new TimeLine(timelineContainer, {tooltip:true}:Config)`; -import type { ApexLog, LogEvent, LogIssue, LogSubCategory } from 'apex-log-parser'; +import type { ApexLog, LogEvent, LogIssue } from 'apex-log-parser'; import { debounce, formatDuration } from '../../../core/utility/Util.js'; import { goToRow } from '../../call-tree/components/CalltreeView.js'; @@ -13,13 +13,12 @@ export interface TimelineGroup { /* eslint-disable @typescript-eslint/naming-convention */ interface TimelineColors { - 'Code Unit': '#88AE58'; - Workflow: '#51A16E'; - Method: '#2B8F81'; - Flow: '#5C8FA6'; - DML: '#B06868'; - SOQL: '#6D4C7D'; - 'System Method': '#8D6E63'; + Method: string; + 'Code Unit': string; + 'System Method': string; + Workflow: string; + DML: string; + SOQL: string; } /* eslint-enable @typescript-eslint/naming-convention */ @@ -38,58 +37,28 @@ interface Rect { const scaleY = -15; const strokeColor = '#D3D3D3'; -export const keyMap: Map = new Map([ - [ - 'Code Unit', - { - label: 'Code Unit', - fillColor: '#88AE58', - }, - ], - [ - 'Workflow', - { - label: 'Workflow', - fillColor: '#51A16E', - }, - ], - [ - 'Method', - { - label: 'Method', - fillColor: '#2B8F81', - }, - ], - [ - 'Flow', - { - label: 'Flow', - fillColor: '#337986', - }, - ], - [ - 'DML', - { - label: 'DML', - fillColor: '#285663', - }, - ], - [ - 'SOQL', - { - label: 'SOQL', - fillColor: '#5D4963', - }, - ], - [ - 'System Method', - { - label: 'System Method', - fillColor: '#5C3444', - }, - ], +export const keyMap: Map = new Map([ + ['Method', { label: 'Method', fillColor: '#2B8F81' }], + ['Code Unit', { label: 'Code Unit', fillColor: '#88AE58' }], + ['System Method', { label: 'System Method', fillColor: '#8D6E63' }], + ['Workflow', { label: 'Workflow', fillColor: '#51A16E' }], + ['DML', { label: 'DML', fillColor: '#B06868' }], + ['SOQL', { label: 'SOQL', fillColor: '#6D4C7D' }], ]); +/* eslint-disable @typescript-eslint/naming-convention */ +const LEGACY_CATEGORY_MAP: Record = { + Apex: 'Method', + 'Code Unit': 'Code Unit', + System: 'System Method', + Automation: 'Workflow', + DML: 'DML', + SOQL: 'SOQL', + Callout: 'Method', + Validation: 'System Method', +}; +/* eslint-enable @typescript-eslint/naming-convention */ + class State { public isRedrawQueued = true; public defaultZoom = 0; @@ -257,7 +226,7 @@ function nodesToRectangles(rootNodes: LogEvent[]) { const nextLevel: LogEvent[] = []; for (const node of currentLevel) { - if (node.duration && node.subCategory) { + if (node.duration && node.category) { addToRectQueue(node, depth); } @@ -278,7 +247,7 @@ function nodesToRectangles(rootNodes: LogEvent[]) { borders.sort((a, b) => a.x - b.x); } } -const rectRenderQueue = new Map(); +const rectRenderQueue = new Map(); const borderRenderQueue = new Map(); let borderSettings = new Map(); let findMatchColor = '#ea5c0054'; @@ -291,10 +260,11 @@ let currentFindMatchColor = '#9e6a03'; */ function addToRectQueue(node: LogEvent, y: number) { const { - subCategory: subCategory, + category, timestamp: x, duration: { total: w }, } = node; + const legacyKey = LEGACY_CATEGORY_MAP[category] ?? category; let borderColor = ''; if (hasFindMatch(node)) { @@ -302,9 +272,9 @@ function addToRectQueue(node: LogEvent, y: number) { } const rect: Rect = { x, y, w, borderColor }; - let list = rectRenderQueue.get(subCategory); + let list = rectRenderQueue.get(legacyKey); if (!list) { - rectRenderQueue.set(subCategory, (list = [])); + rectRenderQueue.set(legacyKey, (list = [])); } list.push(rect); @@ -560,7 +530,8 @@ export function init(timelineContainer: HTMLElement, rootMethod: ApexLog) { export function setColors(timelineColors: TimelineColors) { for (const keyMeta of keyMap.values()) { - const newColor = timelineColors[keyMeta.label as keyof TimelineColors]; + const key = keyMeta.label as keyof TimelineColors; + const newColor = timelineColors[key]; if (newColor) { keyMeta.fillColor = newColor; } @@ -739,7 +710,7 @@ function findTimelineTooltip( return createTooltip( target.text + (target.suffix ?? ''), rows, - keyMap.get(target.subCategory)?.fillColor || '', + keyMap.get(LEGACY_CATEGORY_MAP[target.category] ?? target.category)?.fillColor || '', ); } canvas.classList.add('timeline-hover'); diff --git a/log-viewer/src/features/timeline/themes/Themes.ts b/log-viewer/src/features/timeline/themes/Themes.ts index a85bab7c..a4808446 100644 --- a/log-viewer/src/features/timeline/themes/Themes.ts +++ b/log-viewer/src/features/timeline/themes/Themes.ts @@ -7,22 +7,15 @@ interface TimelineTheme { colors: TimelineColors; } -/** - * Note: Eventual categories will be - * Apex , Code Unit, System, Automation, DML, SOQL, Validation, Callout, Visualforce - */ - export interface TimelineColors { + apex: string; codeUnit: string; - workflow: string; - method: string; - flow: string; + system: string; + automation: string; dml: string; soql: string; - system: string; - reserved1?: string; // Callouts - reserved2?: string; // Validation - reserved3?: string; // Visualforce + callout: string; + validation: string; } export const DEFAULT_THEME_NAME = '50 Shades of Green'; @@ -30,287 +23,248 @@ export const THEMES: TimelineTheme[] = [ { name: '50 Shades of Green Bright', colors: { + apex: '#26A69A', codeUnit: '#9CCC65', - workflow: '#66BB6A', - method: '#26A69A', - flow: '#5BA4CF', + system: '#A1887F', + automation: '#66BB6A', dml: '#E57373', soql: '#BA68C8', - system: '#A1887F', - reserved1: '#FFB74D', - reserved2: '#4DD0E1', - reserved3: '#78909C', + callout: '#FFB74D', + validation: '#5BA4CF', }, }, { name: '50 Shades of Green', colors: { + apex: '#2B8F81', codeUnit: '#88AE58', - workflow: '#51A16E', - method: '#2B8F81', - flow: '#5C8FA6', + system: '#8D6E63', + automation: '#51A16E', dml: '#B06868', soql: '#6D4C7D', - system: '#8D6E63', - reserved1: '#CCA033', - reserved2: '#80CBC4', - reserved3: '#546E7A', + callout: '#CCA033', + validation: '#5C8FA6', }, }, { name: 'Botanical Twilight', colors: { + apex: '#708B91', codeUnit: '#93B376', - workflow: '#5CA880', - method: '#708B91', - flow: '#458593', + system: '#666266', + automation: '#5CA880', dml: '#C26D6D', soql: '#8D7494', - system: '#666266', - reserved1: '#D4A76A', - reserved2: '#A8C2BF', - reserved3: '#566E7A', + callout: '#D4A76A', + validation: '#458593', }, }, { name: 'Catppuccin', colors: { + apex: '#C6A0F6', codeUnit: '#8AADF4', - workflow: '#94E2D5', - method: '#C6A0F6', - flow: '#F5A97F', + system: '#5B6078', + automation: '#94E2D5', dml: '#F38BA8', soql: '#A6DA95', - system: '#5B6078', - reserved1: '#EED49F', - reserved2: '#ED8796', - reserved3: '#F5E0DC', + callout: '#EED49F', + validation: '#F5A97F', }, }, { name: 'Chrome', colors: { + apex: '#EBD272', codeUnit: '#7986CB', - method: '#EBD272', - workflow: '#80CBC4', - flow: '#5DADE2', + system: '#CFD8DC', + automation: '#80CBC4', dml: '#AF7AC5', soql: '#7DCEA0', - system: '#CFD8DC', - reserved1: '#D98880', - reserved2: '#F0B27A', - reserved3: '#90A4AE', + callout: '#D98880', + validation: '#5DADE2', }, }, { name: 'Dracula', colors: { + apex: '#6272A4', codeUnit: '#bd93f9', - workflow: '#8be9fd', - method: '#6272A4', - flow: '#FF79C6', + system: '#44475A', + automation: '#8be9fd', dml: '#FFB86C', soql: '#50FA7B', - system: '#44475A', - reserved1: '#f1fa8c', - reserved2: '#FF5555', - reserved3: '#9580FF', + callout: '#f1fa8c', + validation: '#FF79C6', }, }, { name: 'Dusty Aurora', colors: { + apex: '#56949C', codeUnit: '#455A64', - workflow: '#8CBFA2', - method: '#56949C', - flow: '#7CA5C9', + system: '#8D8078', + automation: '#8CBFA2', dml: '#D68C79', soql: '#A693BD', - system: '#8D8078', - reserved1: '#CDBD7A', - reserved2: '#D67E7E', - reserved3: '#90A4AE', + callout: '#CDBD7A', + validation: '#7CA5C9', }, }, { name: 'Firefox', colors: { + apex: '#D5C266', codeUnit: '#B4B4B9', - workflow: '#C49FCF', - method: '#D5C266', - flow: '#75B5AA', + system: '#8F8585', + automation: '#C49FCF', dml: '#E37F81', soql: '#8DC885', - system: '#8F8585', - reserved1: '#8484D1', - reserved2: '#E8A956', - reserved3: '#5283A4', + callout: '#8484D1', + validation: '#75B5AA', }, }, { name: 'Flame', colors: { + apex: '#F57C00', codeUnit: '#B71C1C', - workflow: '#E65100', - method: '#F57C00', - flow: '#FF7043', + system: '#8D6E63', + automation: '#E65100', dml: '#F44336', soql: '#FFCA28', - system: '#8D6E63', - reserved1: '#C2185B', - reserved2: '#8E24AA', - reserved3: '#5D4037', + callout: '#C2185B', + validation: '#FF7043', }, }, { name: 'Forest Floor', colors: { + apex: '#6D6875', codeUnit: '#2A9D8F', - workflow: '#264653', - method: '#6D6875', - flow: '#E9C46A', + system: '#455A64', + automation: '#264653', dml: '#F4A261', soql: '#E76F51', - system: '#455A64', - reserved1: '#606C38', - reserved2: '#BC4749', - reserved3: '#9C6644', + callout: '#606C38', + validation: '#E9C46A', }, }, { name: 'Garish', colors: { + apex: '#1890FF', codeUnit: '#722ED1', - workflow: '#52C41A', - method: '#1890FF', - flow: '#00BCD4', + system: '#90A4AE', + automation: '#52C41A', dml: '#FF9100', soql: '#EB2F96', - system: '#90A4AE', - reserved1: '#F5222D', - reserved2: '#FFC400', - reserved3: '#651FFF', + callout: '#F5222D', + validation: '#00BCD4', }, }, { name: 'Material', colors: { + apex: '#676E95', codeUnit: '#BA68C8', - workflow: '#4FC3F7', - method: '#676E95', - flow: '#FFCC80', + system: '#A1887F', + automation: '#4FC3F7', dml: '#E57373', soql: '#91B859', - system: '#A1887F', - reserved1: '#F48FB1', - reserved2: '#9FA8DA', - reserved3: '#80CBC4', + callout: '#F48FB1', + validation: '#FFCC80', }, }, { name: 'Modern', colors: { + apex: '#6A7B8C', codeUnit: '#6E7599', - workflow: '#4A918E', - method: '#6A7B8C', - flow: '#C47C46', + system: '#948C84', + automation: '#4A918E', dml: '#CC5E5E', soql: '#5CA376', - system: '#948C84', - reserved1: '#B86B86', - reserved2: '#4D8CB0', - reserved3: '#756CA8', + callout: '#B86B86', + validation: '#C47C46', }, }, { name: 'Monokai Pro', colors: { + apex: '#7B8CA6', codeUnit: '#9E86C8', - workflow: '#FF6188', - method: '#7B8CA6', - flow: '#FFD866', + system: '#9E938D', + automation: '#FF6188', dml: '#FC9867', soql: '#A9DC76', - system: '#9E938D', - reserved1: '#AB9DF2', - reserved2: '#78DCE8', - reserved3: '#8F8B76', + callout: '#AB9DF2', + validation: '#FFD866', }, }, { name: 'Nord', colors: { + apex: '#5e81ac', codeUnit: '#81a1c1', - workflow: '#b48ead', - method: '#5e81ac', - flow: '#d08770', + system: '#4c566a', + automation: '#b48ead', dml: '#bf616a', soql: '#a3be8c', - system: '#4c566a', - reserved1: '#ebcb8b', - reserved2: '#88c0d0', - reserved3: '#8fbcbb', + callout: '#ebcb8b', + validation: '#d08770', }, }, { name: 'Nord Forest', colors: { + apex: '#7B8C7C', codeUnit: '#5E81AC', - workflow: '#EBCB8B', - method: '#7B8C7C', - flow: '#BF616A', + system: '#8C7B7E', + automation: '#EBCB8B', dml: '#D08770', soql: '#B48EAD', - system: '#8C7B7E', - reserved1: '#687585', - reserved2: '#88C0D0', - reserved3: '#81A1C1', + callout: '#687585', + validation: '#BF616A', }, }, { name: 'Okabe-Ito', colors: { + apex: '#56B4E9', codeUnit: '#0072B2', - workflow: '#332288', - method: '#56B4E9', - flow: '#D55E00', + system: '#E69F00', + automation: '#332288', dml: '#CC79A7', soql: '#009E73', - system: '#E69F00', - reserved1: '#882255', - reserved2: '#117733', - reserved3: '#AA4499', + callout: '#882255', + validation: '#D55E00', }, }, { name: 'Salesforce', - colors: { + apex: '#54698D', codeUnit: '#0176D3', - workflow: '#CE4A6B', - method: '#54698D', - flow: '#9050E9', + system: '#706E6B', + automation: '#CE4A6B', dml: '#D68128', soql: '#04844B', - system: '#706E6B', - reserved1: '#D4B753', - reserved2: '#C23934', - reserved3: '#005FB2', + callout: '#D4B753', + validation: '#9050E9', }, }, { name: 'Solarized', colors: { + apex: '#586E75', codeUnit: '#268BD2', - workflow: '#2AA198', - method: '#586E75', - flow: '#6C71C4', + system: '#B58900', + automation: '#2AA198', dml: '#DC322F', soql: '#859900', - system: '#B58900', - reserved1: '#D33682', - reserved2: '#CB4B16', - reserved3: '#93a1a1', + callout: '#D33682', + validation: '#6C71C4', }, }, ]; diff --git a/log-viewer/src/features/timeline/types/flamechart.types.ts b/log-viewer/src/features/timeline/types/flamechart.types.ts index 8c476693..61a9a0fe 100644 --- a/log-viewer/src/features/timeline/types/flamechart.types.ts +++ b/log-viewer/src/features/timeline/types/flamechart.types.ts @@ -11,7 +11,7 @@ //TODO: Remove deps outside timeline -import type { LogEvent, LogSubCategory } from 'apex-log-parser'; +import type { LogCategory, LogEvent } from 'apex-log-parser'; import { formatDuration } from '../../../core/utility/Util.js'; import type { PrecomputedRect } from '../optimised/RectangleManager.js'; @@ -139,7 +139,7 @@ export interface TreeNode { */ export interface RenderBatch { /** Event category this batch represents. */ - category: LogSubCategory; + category: LogCategory; /** PixiJS color value (0xRRGGBB) - pre-blended opaque. */ color: number; @@ -212,7 +212,7 @@ export interface TimelineState { viewport: ViewportState; /** Render batches (7 categories). */ - batches: Map; + batches: Map; /** Cached batch colors for bucket color resolution (performance optimization). */ batchColorsCache: Map; @@ -241,7 +241,7 @@ export interface TimelineState { * Colors can be hex strings ("#RRGGBB"), CSS color names, or rgb/rgba strings. */ export type TimelineColorMap = { - [K in LogSubCategory]?: string; + [K in LogCategory]?: string; }; /** @@ -413,13 +413,14 @@ export const TIMELINE_CONSTANTS = { /** Default color map (matches current Canvas2D colors). */ DEFAULT_COLORS: { + Apex: '#2B8F81', 'Code Unit': '#88AE58', - Workflow: '#51A16E', - Method: '#2B8F81', - Flow: '#5C8FA6', + System: '#8D6E63', + Automation: '#51A16E', DML: '#B06868', SOQL: '#6D4C7D', - 'System Method': '#8D6E63', + Callout: '#CCA033', + Validation: '#5C8FA6', } as TimelineColorMap, /** Maximum zoom level (0.01ms = 10 microsecond visible width in nanoseconds). */ @@ -473,11 +474,12 @@ export const BUCKET_CONSTANTS = { CATEGORY_PRIORITY: [ 'DML', 'SOQL', - 'Method', + 'Callout', + 'Apex', 'Code Unit', - 'System Method', - 'Flow', - 'Workflow', + 'System', + 'Automation', + 'Validation', ] as const, } as const; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/log-viewer/src/features/timeline/utils/tree-converter.ts b/log-viewer/src/features/timeline/utils/tree-converter.ts index b9d1d9ec..7e03e263 100644 --- a/log-viewer/src/features/timeline/utils/tree-converter.ts +++ b/log-viewer/src/features/timeline/utils/tree-converter.ts @@ -126,7 +126,7 @@ function convertEventsRecursive( id, timestamp: event.timestamp, duration: duration, - type: event.type ?? event.subCategory ?? 'UNKNOWN', + type: event.type ?? event.category ?? 'UNKNOWN', text: event.text, original: event, }, @@ -302,7 +302,7 @@ export function logEventToTreeAndRects( id, timestamp: event.timestamp, duration: duration, - type: event.type ?? event.subCategory ?? 'UNKNOWN', + type: event.type ?? event.category ?? 'UNKNOWN', text: event.text, original: event, }, @@ -311,9 +311,9 @@ export function logEventToTreeAndRects( }; // Create PrecomputedRect if event has a valid category - const subCategory = event.subCategory; - if (subCategory) { - const rects = rectsByCategory.get(subCategory); + const category = event.category; + if (category) { + const rects = rectsByCategory.get(category); if (rects) { const rect: PrecomputedRect = { id, @@ -322,7 +322,7 @@ export function logEventToTreeAndRects( depth, duration, selfDuration: event.duration.self, - category: subCategory, + category, eventRef: event, x: 0, y: depth * eventHeight,