Skip to content

Commit ae4dc98

Browse files
Merge pull request #729 from lukecotter/fix-apex-log-detection
fix: improve Apex log detection robustness
2 parents b2db731 + 68e4514 commit ae4dc98

4 files changed

Lines changed: 123 additions & 9 deletions

File tree

lana/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@
7878
"ApexLog",
7979
"DebugLog"
8080
],
81-
"extensions": [],
82-
"firstLine": "^\\d\\d.\\d.+?APEX_CODE,\\w.+$"
81+
"extensions": []
8382
}
8483
],
8584
"menus": {

lana/src/decorations/LogTimingDecoration.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from 'vscode';
1212

1313
import { Context } from '../Context.js';
14-
import { isApexLogContent } from '../language/ApexLogLanguageDetector.js';
14+
import { APEXLOG_HEADER, isApexLogContent } from '../language/ApexLogLanguageDetector.js';
1515
import { formatDuration, TIMESTAMP_REGEX } from '../log-utils.js';
1616

1717
// Pattern to find EXECUTION_STARTED line
@@ -98,8 +98,13 @@ export class LogTimingDecoration {
9898

9999
const formattedDuration = formatDuration(duration);
100100

101-
// Create decoration for line 0 (first line)
102-
const line = document.lineAt(0);
101+
const startLine = this.findFirstLogLine(document);
102+
if (startLine === null) {
103+
editor.setDecorations(decorationType, []);
104+
return;
105+
}
106+
107+
const line = document.lineAt(startLine);
103108
const decoration: DecorationOptions = {
104109
range: line.range,
105110
renderOptions: {
@@ -112,6 +117,17 @@ export class LogTimingDecoration {
112117
editor.setDecorations(decorationType, [decoration]);
113118
}
114119

120+
private findFirstLogLine(doc: TextDocument): number | null {
121+
const limit = Math.min(1000, doc.lineCount);
122+
for (let i = 0; i < limit; i++) {
123+
const text = doc.lineAt(i).text;
124+
if (APEXLOG_HEADER.test(text) || TIMESTAMP_REGEX.test(text)) {
125+
return i;
126+
}
127+
}
128+
return null;
129+
}
130+
115131
private calculateLogDuration(document: TextDocument): number | null {
116132
const startTs = this.findTimestamp(document, false, executionStartedRegex);
117133
const endTs = this.findTimestamp(document, true, TIMESTAMP_REGEX);

lana/src/language/ApexLogLanguageDetector.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616

1717
import { Context } from '../Context.js';
1818

19-
const APEXLOG_HEADER = /^\d\d\.\d.+?APEX_CODE,\w.+$/;
19+
export const APEXLOG_HEADER = /^(\d\d\.\d.+?)?APEX_CODE,\w.+$/;
20+
const EXECUTION_STARTED = /^\d{2}:\d{2}:\d{2}\.\d{1,} \(\d+\)\|EXECUTION_STARTED$/;
21+
const USER_INFO = /^\d{2}:\d{2}:\d{2}\.\d{1,} \(\d+\)\|USER_INFO\|/;
2022
const DETECT_EXTENSIONS = new Set(['.log', '.txt']);
2123
const MAX_LINES_TO_CHECK = 100;
2224

@@ -27,7 +29,8 @@ export function isApexLogContent(doc: TextDocument): boolean {
2729

2830
const linesToCheck = Math.min(MAX_LINES_TO_CHECK, doc.lineCount);
2931
for (let i = 0; i < linesToCheck; i++) {
30-
if (APEXLOG_HEADER.test(doc.lineAt(i).text)) {
32+
const text = doc.lineAt(i).text;
33+
if (APEXLOG_HEADER.test(text) || EXECUTION_STARTED.test(text) || USER_INFO.test(text)) {
3134
return true;
3235
}
3336
}
@@ -47,11 +50,12 @@ function isApexLogFile(fsPath: string): boolean {
4750
const buf = Buffer.alloc(4096);
4851
const bytesRead = readSync(fd, buf, 0, 4096, 0);
4952
const text = buf.toString('utf8', 0, bytesRead);
50-
const lines = text.split('\n');
53+
const lines = text.split(/\r?\n/);
5154

5255
const linesToCheck = Math.min(MAX_LINES_TO_CHECK, lines.length);
5356
for (let i = 0; i < linesToCheck; i++) {
54-
if (APEXLOG_HEADER.test(lines[i] ?? '')) {
57+
const line = lines[i] ?? '';
58+
if (APEXLOG_HEADER.test(line) || EXECUTION_STARTED.test(line) || USER_INFO.test(line)) {
5559
return true;
5660
}
5761
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) 2026 Certinia Inc. All rights reserved.
3+
*/
4+
5+
import { createMockTextDocument } from '../../__tests__/mocks/vscode.js';
6+
import { isApexLogContent } from '../ApexLogLanguageDetector.js';
7+
8+
describe('isApexLogContent', () => {
9+
it('should detect standard log with settings header on line 1', () => {
10+
const doc = createMockTextDocument({
11+
lines: [
12+
'64.0 APEX_CODE,FINE;APEX_PROFILING,NONE;CALLOUT,NONE;DB,INFO;NBA,NONE;SYSTEM,NONE;VALIDATION,NONE;VISUALFORCE,NONE;WAVE,NONE;WORKFLOW,NONE',
13+
'09:45:31.888 (1000)|EXECUTION_STARTED',
14+
],
15+
});
16+
17+
expect(isApexLogContent(doc)).toBe(true);
18+
});
19+
20+
it('should detect log with preamble text before settings header', () => {
21+
const doc = createMockTextDocument({
22+
lines: [
23+
'Some preamble text from browser UI',
24+
'Another line of preamble',
25+
'64.0 APEX_CODE,FINE;APEX_PROFILING,NONE;CALLOUT,NONE;DB,INFO',
26+
'09:45:31.888 (1000)|EXECUTION_STARTED',
27+
],
28+
});
29+
30+
expect(isApexLogContent(doc)).toBe(true);
31+
});
32+
33+
it('should detect log without settings header but with EXECUTION_STARTED', () => {
34+
const doc = createMockTextDocument({
35+
lines: [
36+
'Some preamble text',
37+
'09:45:31.888 (1000)|EXECUTION_STARTED',
38+
'09:45:31.889 (2000)|USER_INFO|[EXTERNAL]|user@example.com',
39+
],
40+
});
41+
42+
expect(isApexLogContent(doc)).toBe(true);
43+
});
44+
45+
it('should detect log without settings header but with USER_INFO', () => {
46+
const doc = createMockTextDocument({
47+
lines: ['Some preamble text', '09:45:31.889 (2000)|USER_INFO|[EXTERNAL]|user@example.com'],
48+
});
49+
50+
expect(isApexLogContent(doc)).toBe(true);
51+
});
52+
53+
it('should detect real-world log starting with USER_INFO and no settings header', () => {
54+
const doc = createMockTextDocument({
55+
lines: [
56+
'17:23:32.3 (3925848)|USER_INFO|[EXTERNAL]|0054R00000B6Q3p|luke.cotter@example.com|(GMT+00:00) Greenwich Mean Time (Europe/London)|GMT+00:00',
57+
'17:23:32.3 (4conversionId)|EXECUTION_STARTED',
58+
],
59+
});
60+
61+
expect(isApexLogContent(doc)).toBe(true);
62+
});
63+
64+
it('should detect log with settings header missing API version', () => {
65+
const doc = createMockTextDocument({
66+
lines: [
67+
'APEX_CODE,FINE;APEX_PROFILING,INFO;CALLOUT,INFO;DB,FINEST;NBA,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,FINE',
68+
'17:23:32.3 (3925848)|USER_INFO|[EXTERNAL]|0054R00000B6Q3p|luke.cotter@example.com|(GMT+00:00) Greenwich Mean Time (Europe/London)|GMT+00:00',
69+
'17:23:32.3 (4000)|EXECUTION_STARTED',
70+
],
71+
});
72+
73+
expect(isApexLogContent(doc)).toBe(true);
74+
});
75+
76+
it('should not detect non-apex log file', () => {
77+
const doc = createMockTextDocument({
78+
lines: [
79+
'[2024-01-15 09:45:31] INFO: Application started',
80+
'[2024-01-15 09:45:32] DEBUG: Loading configuration',
81+
'[2024-01-15 09:45:33] ERROR: Connection failed',
82+
],
83+
});
84+
85+
expect(isApexLogContent(doc)).toBe(false);
86+
});
87+
88+
it('should not detect empty document', () => {
89+
const doc = createMockTextDocument({
90+
lines: [],
91+
});
92+
93+
expect(isApexLogContent(doc)).toBe(false);
94+
});
95+
});

0 commit comments

Comments
 (0)