Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- 🗄️ **Database tab + configurable table columns**: governor-limit visibility, a SOSL table, and column views across tables. ([#162] [#298])
- 📏 **Governor-limit overview** (Database): SOQL, SOSL, DML and query/DML rows shown as `used / limit`, colored as they approach the limit.
- 🧮 **Found vs Counted** (Database): each section reconciles statements found in the log against the governor-counted total, flagging queries that didn't consume the limit (e.g. custom metadata, which is free unless it selects a long text area field or runs in a Flow).
- 🔎 **SOSL table**: a dedicated, searchable Database table.
- 🗂️ **Column views** (Call Tree, Analysis, Database): switch preset column sets, show/hide columns from the **Columns** button or the header right-click menu, inline **reset** to restore defaults; choices persist per view.
- 🏷️ **New columns**: **Object** (queried/target SObject, with group-by) on SOQL/DML; **SOSL Count/Rows**, **Avg Self Time** and optional **Self** variants for every governor metric; **Heap Allocated** (+ Self) via the `Memory` view and Timeline strip; and a SOQL **Query Plan** view (Relative Cost, Leading Operation, SObject Type, Cardinality).
- 🔴 **Timeline exception markers**: exceptions show as red lines, with a **Throws** count in method tooltips. ([#828])
- 🧩 **Call Tree, Analysis and Database tables** gain configurable columns and views. ([#298])
- 🗂️ **Column views**: switch each table between column sets (`General`, `Time`, `Governor Limits`, `Database`, `Memory`); edit a view by showing/hiding columns from the **Columns** toolbar button or the header right-click menu, with inline **reset** to restore defaults; choices persist per view.
- 📊 **New columns**: **SOSL Count/Rows**, **Avg Self Time**, and optional **Self** variants for every governor metric.
- 🧠 **Heap / memory analysis**: **Heap Allocated** (+ Self) columns, surfaced through the `Memory` view and the Timeline governor limit strip.
- 🔎 **SOQL Query Plan** view: Relative Cost, Leading Operation, SObject Type and Cardinality.

### Changed

- ♻️ Replaced the deprecated `webview-ui-toolkit` with [vscode-elements](https://github.com/vscode-elements/elements) for all UI controls. ([#576]).
- 🎛️ **Modernised dropdowns**: searchable, compact controls that carry the field and value in one place (e.g. `Group: Namespace`, `Type: All`) ([#848]).
- 📊 **Timeline governor limits**: tooltip rows keep a stable order and always show the `used / limit` value, so figures no longer jump around as you move the pointer. ([#827])
- 🚧 **Timeline truncation markers** now end where the log recovers, so trusted sections are no longer greyed out. ([#828])
- 🎛️ **Modernised dropdowns**: searchable, compact controls that carry the field and value in one place (e.g. `Group: Namespace`, `Type: All`) ([#848]).
- 🗂️ **Call Tree + Database styling**: VS Code style tree icons, and rows indent under their group headings. ([#832]).
- ♻️ Replaced the deprecated `webview-ui-toolkit` with [vscode-elements](https://github.com/vscode-elements/elements) for all UI controls. ([#576]).

## [1.20.0] 2026-06-18

Expand Down Expand Up @@ -511,6 +512,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea
[#848]: https://github.com/certinia/debug-log-analyzer/issues/848
[#827]: https://github.com/certinia/debug-log-analyzer/issues/827
[#298]: https://github.com/certinia/debug-log-analyzer/issues/298
[#162]: https://github.com/certinia/debug-log-analyzer/issues/162
[#32]: https://github.com/certinia/debug-log-analyzer/issues/32

<!-- v1.20.0 -->
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,17 @@ See which methods are the slowest, most frequent. or expensive.

## 🗄️ Database Analysis

Highlight slow Salesforce SOQL queries, non-selective filters, and DML issues.
Highlight slow Salesforce SOQL queries, non-selective filters, and DML issues, and see how each contributes to governor limits.

- **SOQL + DML Duration, Selectivity, Aggregates, Row Count**
- **Group by Namespace, Caller Namespace or Query**
- **Governor limit overview** – SOQL, SOSL, DML and query/DML rows shown as `used / limit` at the top of the tab.
- **Tracked vs consumed** – each section reconciles statements seen in the log against the governor-counted total, so you can spot queries that didn't count (e.g. custom metadata).
- **Separate SOQL, DML and SOSL tables** – SOSL is fully searchable, with its rows metered against the 2,000-per-query cap.
- **Object column + Group by Object / Namespace / Caller Namespace / Query**
- **SOQL Duration, Selectivity, Aggregates, Row Count**
- **Column Views** – Preset column sets (incl. a SOQL Query Plan view), show/hide columns, reset to defaults
- **View the Call Stack**
- **SOQL Optimization Tips**
- **Sort by SOQL or DML, Duration, Selectivity, Aggregates, Row Count**
- **Copy or Export to CSV**
- **View the Call Stack**, **SOQL Optimization Tips**, **Sort**, **Copy or Export to CSV**

<!-- TODO: re-capture database.png for the redesigned Database tab (governor strip, DML/SOQL/SOSL sections). Optionally add a sosl.png. -->

![Database](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/assets/1_20/database.png)

Expand Down
18 changes: 16 additions & 2 deletions apex-log-parser/__tests__/EventMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/
import { describe, expect } from '@jest/globals';
import { getLogEventClass, parse } from '../src/index.js';
import { describe, expect, it } from '@jest/globals';
import { DMLBeginLine, getLogEventClass, parse } from '../src/index.js';

describe('Event debugLevel and debugCategory', () => {
describe('new events are parsed (not null from getLogEventClass)', () => {
Expand Down Expand Up @@ -89,4 +89,18 @@ describe('Event debugLevel and debugCategory', () => {
expect(line!.debugLevel).toBe(expectedLevel);
});
});

describe('DMLBeginLine.sObjectType', () => {
it.each([
['15:20:52.222 (100)|DML_BEGIN|[1]|Op:Insert|Type:Account|Rows:1', 'Account'],
[
'15:20:52.222 (100)|DML_BEGIN|[1]|Op:Update|Type:ns2__MyObject__c|Rows:1',
'ns2__MyObject__c',
],
])('parses the target object from %s', (logLine, expected) => {
const line = parse(logLine).children[0];
expect(line).toBeInstanceOf(DMLBeginLine);
expect((line as DMLBeginLine).sObjectType).toBe(expected);
});
});
});
6 changes: 6 additions & 0 deletions apex-log-parser/src/LogEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,11 +962,17 @@ export class DMLBeginLine extends DurationLogEvent {
total: 1,
};
namespace = 'default';
/** The SObject the DML targets (e.g. `Account`), from the `Type:` field. Null if absent. */
sObjectType: string | null = null;

constructor(parser: ApexLogParser, parts: string[]) {
super(parser, parts, ['DML_END'], LOG_CATEGORY.DML, 'free');
this.lineNumber = this.parseLineNumber(parts[2]);
this.text = 'DML ' + parts[3] + ' ' + parts[4];
const typePart = parts[4];
if (typePart?.startsWith('Type:')) {
this.sObjectType = typePart.slice('Type:'.length);
}
const rowCountString = parts[5];
this.dmlRowCount.total = this.dmlRowCount.self = rowCountString ? parseRows(rowCountString) : 0;
}
Expand Down
1 change: 1 addition & 0 deletions apex-log-parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export {
MethodEntryLine,
SOQLExecuteBeginLine,
SOQLExecuteExplainLine,
SOSLExecuteBeginLine,
parseObjectNamespace,
parseRows,
parseVfNamespace,
Expand Down
29 changes: 21 additions & 8 deletions lana-docs/docs/docs/features/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ description: Database insights help Salesforce developers analyze SOQL and DML o
keywords:
[
salesforce database analysis,
soql optimization,
dml performance,
soql,
sosl,
dml,
optimization,
performance,
apex query tuning,
salesforce log analysis,
database insights,
Expand All @@ -19,9 +22,19 @@ hide_title: true

## 💾 Database

Database insights help Salesforce developers analyze SOQL and DML operations, assess query selectivity, performance, and aggregations, and optimize Apex code using advanced sorting, grouping, filtering, call stack tracing, and CSV export tools.
Database insights help Salesforce developers analyze DML, SOQL and SOSL operations, assess query selectivity, performance, and aggregations, and optimize Apex code using advanced sorting, grouping, filtering, call stack tracing, and CSV export tools.

![Database view screenshot displaying SOQL and DML operations with row counts, execution times, selectivity indicators, and aggregation details for Salesforce log analysis.](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/assets/1_20/database.png)
![Database view screenshot displaying DML, SOQL and SOSL operations with row counts, execution times, selectivity indicators, and aggregation details for Salesforce log analysis.](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/assets/1_20/database.png)

The tab has separate **DML**, **SOQL** and **SOSL** sections

### Governor limits

- **Overview strip** — SOQL, SOSL, DML and query/DML rows as `used / limit`, coloured as they near the limit.
- **Tracked vs consumed** — per section, statements seen in the log vs the number Salesforce counted (`CUMULATIVE_LIMIT_USAGE`). A gap is usually custom metadata (`__mdt`), which is free unless the query selects a long text area field or runs in a Flow.
- **SOSL rows** — metered per query against the 2,000-rows-per-query cap.

> Consumed figures need the Apex Profiling log category. Without it, sections show the tracked count and mark the limit _n/a_.

The _Selectivity_ column will have a green tick if the query is selective, a red cross if it is not and will be blank if the selectivity could not be determine. Sorting on this column will sort the rows by relative query cost, this number can be seen by hovering the cell on the selectivity column.

Expand All @@ -39,15 +52,15 @@ If the grouping is removed the sorting applies the same but across all rows inst

### Column Views

Switch column sets from the **Columns** button in the toolbar (or the header right-click menu). SOQL offers **General**, **Performance** and **Query Plan** (Relative Cost, Leading Operation, SObject Type, Cardinality); DML offers **General** and **Timing**. Show or hide individual columns there; an edited view shows a **reset** icon. Choices persist per table.
Switch column sets from the **Columns** button in the toolbar (or the header right-click menu). SOQL offers **General** (incl. the **Object** column), **Performance**, **Query Plan** (Relative Cost, Leading Operation, SObject Type, Cardinality) and **Limits**; DML offers **General**, **Timing** and **Limits**; SOSL offers **General** and **Timing**. Show or hide individual columns there; an edited view shows a **reset** icon. Choices persist per table.

### Group

By default rows are grouped by the SOQL/ DML text, grouping can be removed and the rows shows as a flat list using the _Group by_ item in the header menu. The groups are default sorted with the groups with the most items at the top.
By default rows are grouped by the SOQL/ DML text, grouping can be removed and the rows shows as a flat list using the _Group by_ dropdown. The groups are default sorted with the groups with the most items at the top.

SOQL Statements can also be grouped by package namespace including the default namespace
SOQL and DML can be grouped by **Object** (the queried/target SObject), **Namespace**, or **Caller Namespace**, as well as the statement text. SOSL can be grouped by Namespace or Caller Namespace.

Rows can also be grouped by **Caller Namespace** the namespace of the direct caller that issued the DML. This makes it easy to see which package's code is responsible for that DML, even though the time is attributed to the default namespace.
**Caller Namespace** is the namespace of the direct caller that issued the statement — handy for seeing which package's code is responsible, even when the time is attributed to the default namespace.

### DML / SOQL Call Stack

Expand Down
17 changes: 15 additions & 2 deletions lana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,8 @@
"enum": [
"General",
"Performance",
"Query Plan"
"Query Plan",
"Limits"
],
"markdownDescription": "The column view applied to the Database SOQL table. Default: `General`",
"order": 0
Expand All @@ -365,10 +366,22 @@
"default": "General",
"enum": [
"General",
"Timing"
"Timing",
"Limits"
],
"markdownDescription": "The column view applied to the Database DML table. Default: `General`",
"order": 2
},
"lana.database.sosl.columnView": {
"title": "SOSL column view",
"type": "string",
"default": "General",
"enum": [
"General",
"Timing"
],
"markdownDescription": "The column view applied to the Database SOSL table. Default: `General`",
"order": 4
}
}
}
Expand Down
1 change: 1 addition & 0 deletions lana/src/commands/LogView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class LogView {
config.callTree.columnOverrides = overrides['callTree.columnOverrides'] ?? {};
config.database.soql.columnOverrides = overrides['database.soql.columnOverrides'] ?? {};
config.database.dml.columnOverrides = overrides['database.dml.columnOverrides'] ?? {};
config.database.sosl.columnOverrides = overrides['database.sosl.columnOverrides'] ?? {};
panel.webview.postMessage({
requestId,
cmd: 'getConfig',
Expand Down
2 changes: 2 additions & 0 deletions lana/src/workspace/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface Config {
database: {
soql: { columnView: string; columnOverrides: Record<string, string[]> };
dml: { columnView: string; columnOverrides: Record<string, string[]> };
sosl: { columnView: string; columnOverrides: Record<string, string[]> };
};
}

Expand Down Expand Up @@ -75,6 +76,7 @@ export const COLUMN_OVERRIDE_SECTIONS = [
'callTree.columnOverrides',
'database.soql.columnOverrides',
'database.dml.columnOverrides',
'database.sosl.columnOverrides',
] as const;

type ColumnOverrides = Record<string, string[]>;
Expand Down
18 changes: 18 additions & 0 deletions log-viewer/src/__tests__/Database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ describe('Analyse database tests', () => {

const firstDML = result.getDMLLines()[0];
expect(firstDML?.text).toEqual('DML Op:Insert Type:codaCompany__c');
expect(firstDML?.sObjectType).toEqual('codaCompany__c');
});

it('collects SOSL statements', async () => {
const log =
'09:18:22.6 (6574780)|EXECUTION_STARTED\n' +
'09:18:22.6 (6586704)|CODE_UNIT_STARTED|[EXTERNAL]|066d0000002m8ij|apex://pkg.Entry\n' +
'17:33:36.2 (1672655920)|SOSL_EXECUTE_BEGIN|[12]|FIND :searchQuery RETURNING Account(Id, Name)\n' +
'17:33:36.2 (1678684460)|SOSL_EXECUTE_END|[12]|Rows:5\n' +
'09:18:22.6 (7300000)|CODE_UNIT_FINISHED|apex://pkg.Entry\n' +
'09:18:22.6 (7400000)|EXECUTION_FINISHED\n';

const apexLog = parse(log);
const result = await DatabaseAccess.create(apexLog);

const soslLines = result.getSOSLLines();
expect(soslLines.length).toEqual(1);
expect(soslLines[0]?.soslRowCount.self).toEqual(5);
});

it('resolves stack by eventIndex when timestamps are duplicated', async () => {
Expand Down
5 changes: 3 additions & 2 deletions log-viewer/src/features/call-tree/utils/GovernorCost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ interface CostMetric {
* Metrics that are attributed per call-tree node (and therefore comparable to
* their limit per path). CPU time and callouts are intentionally excluded —
* they are only tracked globally, not per node. SOSL rows are also excluded:
* they have no governor limit (only SOSL *queries* is limited, to 20) and do
* not count against the SOQL query-rows limit.
* they have no per-transaction limit to accumulate against (the 2,000-row cap
* is per query) and don't count against the SOQL query-rows limit; only SOSL
* *queries* is a transaction total (limited to 20).
*/
const COST_METRICS: CostMetric[] = [
{ label: 'SOQL', used: (r) => r.soqlCount.total, limit: (l) => l.soqlQueries.limit },
Expand Down
27 changes: 27 additions & 0 deletions log-viewer/src/features/database/__tests__/limits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/
import { SOSL_ROWS_PER_QUERY_LIMIT, soslRowsMetric } from '../limits.js';

describe('soslRowsMetric', () => {
it('degrades to "limit n/a" (used null, no ceiling, no note) with no cumulative snapshot', () => {
const metric = soslRowsMetric(500, 0, false);
expect(metric.used).toBeNull();
expect(metric.limit).toBe(0);
expect(metric.note).toBeUndefined();
});

it('derives the ceiling from soslQueries.limit × per-query cap when a snapshot is present', () => {
const metric = soslRowsMetric(500, 20, true);
expect(metric.used).toBe(500);
expect(metric.limit).toBe(20 * SOSL_ROWS_PER_QUERY_LIMIT);
expect(metric.note).toContain('max per transaction');
});

it('shows no ceiling when a snapshot exists but the SOSL-query limit is 0', () => {
const metric = soslRowsMetric(500, 0, true);
expect(metric.used).toBe(500); // count is trusted (snapshot present)
expect(metric.limit).toBe(0); // but no meaningful ceiling to meter against
expect(metric.note).toBeUndefined();
});
});
Loading