case 4703 - report script console.error details in render errors

The page log showed console.error objects only as JSHandle@object and errors
without their stack. Each ERROR line is still logged at once; object and
Error arguments are then fetched from the page and added as detail lines
(object JSON, error stack without Puppeteer frames and with file names only).
A failing render waits up to 2 s for pending details. Page log appends are
locked. Report designer manual (Script errors) and changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 08:14:52 -07:00
parent 64edeea874
commit c55b2fc723
4 changed files with 83 additions and 16 deletions

View File

@@ -212,7 +212,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s
| 4697 | Relative date filters using the server's time zone | 1. **Done** (`BuildSQLBetweenTwoTokenDerivedDates` converts the user's local boundaries to UTC with `clientTimeStamp.Offset`, was `ToUniversalTime()` with the server's zone; the dashboard KPIs use the same builder, so their ranges are fixed too. Test `WorkOrderList_TodayIsTheUsersToday`: a work order at 00:30 today for a +10:00 user, before missing from *today*, after included; full suite = baseline. The manual already promised the user's time zone (`ay-start-data-types.md`, `ay-start-form-data-tables.md`), no manual change; changelog) |
| 4699 | Linux desktop data-path note in the manual | 1, docs |
| 4700 | Restore note for customers with a dedicated database role | 1, docs |
| 4703 | Report JS console error logging | 1 |
| 4703 | Report JS console error logging | 1. **Done** (error messages were already logged; what was lost: object arguments shown as `JSHandle@object`, no stacks. Now each `ERROR:` line is logged at once as before, then each object or Error argument is fetched from the page in the background and added as a `detail for "<message start>":` line (object JSON cut at 1000 characters; error stack without Puppeteer frames, paths shortened to file names); a failing render waits up to 2 s for pending details; page log appends locked (console thread, relay). Better than Sockeye's version, which loses the whole line when fetching details fails. Test `ConsoleErrorDetailsAreLogged`; 22 stock reports unchanged; manual (report designer, Script errors) and changelog) |
| 4705 | License.DoFetchAsync never tries the fifth licence server | 1. **Done** (rethrow after the last server, `x == 4`; the trial request (`rvr`, rockfish only) now uses the same fallback loop, John's decision. Sandbox with rockfish, io, europa and callisto pointed at 127.0.0.1: the old build failed the trial request with a connection error, the new build reached sockeye.ayanova.com. No changelog: only noticeable if the licence server names fail) |
| 4706 | Remove the unused RunProgram.Run | 1. **Done** (`Run` and its `RunWindows`/`RunLinuxShell`/`RunOSX` removed, no callers; `RunNoShell` stays. Backup through the API wrote the database dump and attachments zip; full suite = baseline) |
| 4707 | Stray cypress.json in raven-client | 1. **Done** (raven-client `f2f10b81`: removed; from 2020, pointed at `tests/e2e`, which moved to `raven-test` in 2022 (`e0be8a7c`); Cypress is not a client dependency) |
@@ -274,7 +274,7 @@ Found during the port, out of scope. One line each.
- **Own case after the port (John): automated provisioning from Sockeye.** Goal: from a trial subscription server request, one menu item in Sockeye (after John's manual approval) provisions the whole server through the DigitalOcean API, ready for login: droplet, DNS, firewall, setup, licence. Replaces today's manual steps (header comments of the setup script), which are slow and error-prone. Not part of this port
- Linux desktop manual (`ops-install-linux-desktop.md`), pre-existing, same in 8.2.4: `sudo mkdir ~/.ayanova-data` creates the folder in the user's home, but `sudo ./AyaNova` runs as root, so `AYANOVA_DATA_PATH` `~/.ayanova-data` resolves to `/root/.ayanova-data` and the user's folder is never used (data, logs and backups end up under `/root`). Found in the 8.3.0 desktop zip test. Fix the manual (absolute data path, or not running as root) after the release
- **Own case after the port (John):** the subscription server setup script is hard-coded in `biz-subscription-server.vue`, so every change needs a new Sockeye client build. Make it editable without a rebuild
- **Improvement, own case after the port (John to create):** bring back logging of JavaScript error details from report page console errors, as Sockeye's port did. AyaNova logs only `ERROR: {message text}`; the per-argument detail (`arg instanceof Error ? arg.message : arg`) is commented out at `biz/ReportBiz.cs:873-876` since case 4586, because `IJSHandle.ExecutionContext` was removed. Sockeye 8.1.0 (`server/biz/ReportBiz.cs:575`) evaluates on the handle instead: `args.Message.Args.Select(arg => arg.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg"))`, which also needs the console handler to be `async` again
- **Done in case 4703.** **Improvement, own case after the port (John to create):** bring back logging of JavaScript error details from report page console errors, as Sockeye's port did. AyaNova logs only `ERROR: {message text}`; the per-argument detail (`arg instanceof Error ? arg.message : arg`) is commented out at `biz/ReportBiz.cs:873-876` since case 4586, because `IJSHandle.ExecutionContext` was removed. Sockeye 8.1.0 (`server/biz/ReportBiz.cs:575`) evaluates on the handle instead: `args.Message.Args.Select(arg => arg.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg"))`, which also needs the console handler to be `async` again
- Pre-existing, same in 8.2.4 (found in the 4579 sandbox test, John: Deferred): rendering a report for a record that does not exist (e.g. work order ids 1 and 2 in a trial without sample data) fails the job with a generic `ArgumentNullException` ("Value cannot be null. (Parameter 'o')") from `JObject.FromObject` in `WorkOrderBiz.GetReportData` (`WorkOrderBiz.cs:1030`), instead of skipping the missing record or saying it is not found. Other object types' `GetReportData` likely do the same
- **Roles audit, case 4735 (John, 2026-10-25, this release; found in 4719):** check every object's rights as documented in the manual (each page's "Authorization Roles required" section and `ay-biz-admin-roles.md`) against `biz/BizRoles.cs`. Report templates were changeable by Business administration - restricted although the roles page uses exactly that case as its example of a view-only restricted role (fixed in 4719). If that slipped through there could be others; for each mismatch decide whether the code or the page is wrong
- From case 4697 (John: Deferred): (1) a relative date range that crosses a daylight saving change can be an hour off at its far end, because the client sends only its current offset with list requests; exact would need the client to send its IANA time zone name too (front-end change). (2) Dashboard widgets group by day, week or month with `date_trunc` on the stored UTC times, so for a user far from UTC an item near midnight lands in the neighbouring bar; same need for the time zone name

View File

@@ -176,6 +176,20 @@ Note that the SAMPLE DATA section of the user interface in the report designer s
A common use for the ayPrepareData function would be to insert or re-shape data based on calculations or data fetched from external API's or the AyaNova API itself (see the section regarding API usage below for details). It is defined as an asynchronous function so that the User can all AyaNova or other external API's if necessary to fetch data to insert into the report. For example getting part images from a manufacturers API site to insert into a report document.
#### Script errors
When a report fails to render, the error shown includes everything the report's scripts wrote with `console.error` (and `console.log` / `console.warn`). For `console.error`, objects passed to it are also shown as their contents and errors with their stack, for example:
```javascript
try {
// ...
} catch (e) {
console.error("Getting part images failed", e, { partId: item.PartId });
}
```
In a stack line such as `at ayPrepareData (<anonymous>:12:30)` the numbers are the line and column within your Prepare or Helpers script.
### Helpers
![report edit form helpers tab](img/report-edit-form-helpers-tab.png)

View File

@@ -32,6 +32,7 @@ IN PROGRESS
- Server: while a report renders, its template no longer holds the User's actual access token or download key; the AyaNova server adds the User's access only to the report's requests to the `ayApiUrl` address. Templates that call the AyaNova API through any other address need to be changed. See [API usage in reports](ay-report-edit.md#api-usage).
- Server: users with the "Business administration - restricted" role can view [report templates](adm-report-templates.md) but no longer create, edit or import them, as described for that [role](ay-biz-admin-roles.md). Give the full "Business administration" role to users who edit report templates.
- Server: relative date filters such as "today" or "last month" (in lists and dashboard widgets) now start and end the period at midnight in the user's own [time zone](ay-start-form-data-tables.md#filtering-dates-and-time-zones). Previously, for a user in a different time zone from the server, records close to the start or end of the period could be included or left out by mistake.
- Server: when a report fails to render, objects and errors a report script passes to `console.error` are now shown with their contents and stack (line numbers within the script) in the error message, to help with [writing report scripts](ay-report-edit.md#script-errors).
- Server: the SuperUser account can no longer be deleted.
- Server: role rights corrected to match the documentation: "Service technician - restricted" users can view tax codes, projects, unit models and meter readings; "Business administration" users can view server metrics; Accounting users can no longer enter unit meter readings; only "Business administration" users can switch an [integrated application](api-integrated-applications.md) on or off or delete it.
- Server: fixed direct notification messages to a user: only the first one within 12 hours was delivered, now every message is.

View File

@@ -898,6 +898,14 @@ What could be the issue here?
}
log.LogDebug($"Report browser launch arguments: '{string.Join(" ", lo.Args)}'");
System.Text.StringBuilder PageLog = new System.Text.StringBuilder();
//case 4703: console events and the API relay add to the page log from other threads
void AppendPageLog(string line)
{
lock (PageLog)
PageLog.AppendLine(line);
}
//case 4703: details of console.error arguments are fetched from the page after the error line is logged
var PendingConsoleDetails = new List<Task>();
if (!ReportRenderManager.KeepGoing(job.GId))
return;
@@ -930,22 +938,21 @@ What could be the issue here?
switch (args.Message.Type)
{
case ConsoleType.Error:
try
AppendPageLog($"ERROR: {args.Message.Text}");
//case 4703: the message text shows an object only as JSHandle@object and an error without its stack, so
//for each argument the page is asked for an error's stack or an object's JSON; added as detail lines
if (args.Message.Args != null && args.Message.Args.Any())
{
//case 4586 todo force an error here and see what we can do with it old vs new behaviour test on current release same report with erorr triggered
//ok, I tested it with an exception and it worked just find so shrug emoji
PageLog.AppendLine($"ERROR: {args.Message.Text}");
//case 4586 this no longer works due to no executioncontext in arg in teh block below
// var errorArgs = await Task.WhenAll(args.Message.Args.Select(arg => arg.ExecutionContext.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg", arg)));
// PageLog.AppendLine($"ERROR: {args.Message.Text} args: [{string.Join<object>(", ", errorArgs)}]");
var detailTask = AppendConsoleErrorDetailsAsync(args.Message.Text ?? string.Empty, args.Message.Args, AppendPageLog);
lock (PendingConsoleDetails)
PendingConsoleDetails.Add(detailTask);
}
catch { }
break;
case ConsoleType.Warning:
PageLog.AppendLine($"WARNING: {args.Message.Text}");
AppendPageLog($"WARNING: {args.Message.Text}");
break;
default:
PageLog.AppendLine($"INFO: {args.Message.Text}");
AppendPageLog($"INFO: {args.Message.Text}");
break;
}
@@ -962,7 +969,7 @@ What could be the issue here?
pageClientMeta["DownloadToken"] = ReportApiRelay.DOWNLOAD_KEY_PLACEHOLDER;
//case 4632: inside IIS the page's API calls are also relayed by the server
log.LogDebug($"Handling report API calls under {apiUrl}{(relayTarget == null ? "" : $", relayed to {relayTarget}")}");
relayClient = await ReportApiRelay.AttachAsync(page, apiUrl, relayTarget, realAuthorization, realDownloadKey, TimeSpan.FromMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), s => PageLog.AppendLine(s), log);
relayClient = await ReportApiRelay.AttachAsync(page, apiUrl, relayTarget, realAuthorization, realDownloadKey, TimeSpan.FromMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), AppendPageLog, log);
log.LogDebug($"Preparing page: adding base reporting scripts to page");
if (!ReportRenderManager.KeepGoing(job.GId))
@@ -1258,18 +1265,27 @@ What could be the issue here?
//This is the error when a helper is used on the template but doesn't exist:
//Evaluation failed: d
//(it might also mean other things wrong with template)
if (PageLog.Length > 0)
//case 4703: let console.error details still being fetched reach the page log (at most 2 seconds)
Task[] pendingDetails;
lock (PendingConsoleDetails)
pendingDetails = PendingConsoleDetails.ToArray();
if (pendingDetails.Length > 0)
await Task.WhenAny(Task.WhenAll(pendingDetails), Task.Delay(2000));
string pageLogText;
lock (PageLog)
pageLogText = PageLog.ToString();
if (pageLogText.Length > 0)
{
log.LogInformation(
$"Exception caught while rendering report \"{report.Name}\", report Page console log:"
);
log.LogInformation(PageLog.ToString());
log.LogInformation(pageLogText);
var json = Newtonsoft.Json.JsonConvert.SerializeObject(
new
{
rendererror = new
{
pagelog = PageLog.ToString(),
pagelog = pageLogText,
exception = ExceptionUtil.ExtractAllExceptionMessages(ex),
},
},
@@ -1345,6 +1361,42 @@ What could be the issue here?
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed);
}
//case 4703: for each console.error argument that is an Error (its stack: message, file and line) or an object (its
//JSON, cut at 1000 characters) adds an indented detail line to the page log, labelled with the start of the error's
//message text as details arrive after the error lines; other values are already in the message text.
//Stack lines from Puppeteer's own evaluation code are left out and file paths shortened to the file name
static async Task AppendConsoleErrorDetailsAsync(string messageText, IEnumerable<IJSHandle> consoleArgs, Action<string> appendPageLog)
{
const string detailScript = @"(arg) => {
if (arg instanceof Error) {
const s = arg.stack || String(arg);
return s.split('\n')
.filter(l => !/__puppeteer_evaluation_script__|RAFPoller|createFunction/.test(l))
.map(l => l.replace(/\((?:[^()]*[\\\/])?([^\\\/()]+:\d+:\d+)\)/, '($1)'))
.join('\n');
}
if (arg !== null && typeof arg === 'object') {
try { const s = JSON.stringify(arg); return s && s.length > 1000 ? s.substring(0, 1000) + '...' : s; }
catch (e) { return String(arg); }
}
return null;
}";
var label = messageText.Length > 60 ? messageText.Substring(0, 60) + "..." : messageText;
foreach (var arg in consoleArgs)
{
try
{
var detail = await arg.EvaluateFunctionAsync<string>(detailScript);
if (!string.IsNullOrWhiteSpace(detail))
appendPageLog($" detail for \"{label}\": " + detail.Replace("\n", "\n "));
}
catch
{
//the page may already be closed; the error line itself is logged regardless
}
}
}
//Report browser could not be downloaded or started: log it properly and give the client a specific error
//instead of the generic job failure it got when this escaped to the job processor
static async Task HandleBrowserFailure(OpsJob job, ILogger log, string what, Exception ex)