chore: add pre-blazor crash diagnostics
This commit is contained in:
@@ -195,6 +195,7 @@ SQLite migration rule:
|
|||||||
- Workspace reads are resolved through API requests in `WorkspaceQueryService`; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers.
|
- Workspace reads are resolved through API requests in `WorkspaceQueryService`; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers.
|
||||||
- Interactive authenticated startup begins in `WorkspaceRouteView.razor` after first render because `RpgRollerApiClient` still depends on JS interop-backed `fetch`.
|
- Interactive authenticated startup begins in `WorkspaceRouteView.razor` after first render because `RpgRollerApiClient` still depends on JS interop-backed `fetch`.
|
||||||
- Workspace startup diagnostics now log route initialization, route-content render phases, and browser-side workspace mutation snapshots to help isolate the remaining Firefox startup crash documented in `POSTMORTEM.md`.
|
- Workspace startup diagnostics now log route initialization, route-content render phases, and browser-side workspace mutation snapshots to help isolate the remaining Firefox startup crash documented in `POSTMORTEM.md`.
|
||||||
|
- Pre-Blazor diagnostics also watch the static `#rr-interactive-host` container before `_framework/blazor.web.js` connects, so extension-driven DOM mutations can be compared against the first failing interactive batch.
|
||||||
- Live workspace refresh compares separate roster, per-character sheet, and log versions so unrelated changes do not trigger full reloads.
|
- Live workspace refresh compares separate roster, per-character sheet, and log versions so unrelated changes do not trigger full reloads.
|
||||||
- Campaign log data is loaded in bounded slices: campaign summaries, one selected roster, one selected character sheet, and a 25-row incremental log window from `/api/campaigns/{campaignId}/log/page`.
|
- Campaign log data is loaded in bounded slices: campaign summaries, one selected roster, one selected character sheet, and a 25-row incremental log window from `/api/campaigns/{campaignId}/log/page`.
|
||||||
- Log rows return compact summary data first and lazy-load full detail from `/api/rolls/{rollId}` when expanded.
|
- Log rows return compact summary data first and lazy-load full detail from `/api/rolls/{rollId}` when expanded.
|
||||||
|
|||||||
@@ -25,11 +25,16 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div id="rr-interactive-host" data-request-path="@RequestPath">
|
||||||
<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))"/>
|
<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))"/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<script src="js/rpgroller-api.js"></script>
|
<script src="js/rpgroller-api.js"></script>
|
||||||
@if (UseInteractiveApp)
|
@if (UseInteractiveApp)
|
||||||
{
|
{
|
||||||
|
<script>
|
||||||
|
window.rpgRollerApi.bootstrapPreBlazorDiagnostics("@RequestPath");
|
||||||
|
</script>
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
}
|
}
|
||||||
</body>
|
</body>
|
||||||
@@ -55,6 +60,8 @@ else
|
|||||||
|
|
||||||
private bool AuthStatusIsError => string.Equals(ReadAuthQueryValue("kind"), "error", StringComparison.OrdinalIgnoreCase);
|
private bool AuthStatusIsError => string.Equals(ReadAuthQueryValue("kind"), "error", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private string RequestPath => HttpContext?.Request.Path.Value ?? "/";
|
||||||
|
|
||||||
private string BaseHref
|
private string BaseHref
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ window.rpgRollerApi = (() => {
|
|||||||
observer: null,
|
observer: null,
|
||||||
route: null,
|
route: null,
|
||||||
globalHandlersInstalled: false,
|
globalHandlersInstalled: false,
|
||||||
|
domOperationDiagnosticsInstalled: false,
|
||||||
mutationBatchCount: 0
|
mutationBatchCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,6 +25,14 @@ window.rpgRollerApi = (() => {
|
|||||||
console.warn(debugPrefix, new Date().toISOString(), ...args);
|
console.warn(debugPrefix, new Date().toISOString(), ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function summarizeChildren(element) {
|
||||||
|
if (!element || !element.childNodes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(element.childNodes).slice(0, 20).map(summarizeNode);
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeNode(node) {
|
function summarizeNode(node) {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return "<null>";
|
return "<null>";
|
||||||
@@ -117,6 +126,53 @@ window.rpgRollerApi = (() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function installDomOperationDiagnostics() {
|
||||||
|
if (workspaceDiagnostics.domOperationDiagnosticsInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceDiagnostics.domOperationDiagnosticsInstalled = true;
|
||||||
|
const originalInsertBefore = Node.prototype.insertBefore;
|
||||||
|
const originalAppendChild = Node.prototype.appendChild;
|
||||||
|
const originalRemoveChild = Node.prototype.removeChild;
|
||||||
|
const originalReplaceChild = Node.prototype.replaceChild;
|
||||||
|
|
||||||
|
Node.prototype.insertBefore = function (newNode, referenceNode) {
|
||||||
|
debug("dom insertBefore", {
|
||||||
|
parent: summarizeNode(this),
|
||||||
|
newNode: summarizeNode(newNode),
|
||||||
|
referenceNode: summarizeNode(referenceNode),
|
||||||
|
referenceParent: referenceNode ? summarizeNode(referenceNode.parentNode) : null
|
||||||
|
});
|
||||||
|
return originalInsertBefore.call(this, newNode, referenceNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
Node.prototype.appendChild = function (child) {
|
||||||
|
debug("dom appendChild", {
|
||||||
|
parent: summarizeNode(this),
|
||||||
|
child: summarizeNode(child)
|
||||||
|
});
|
||||||
|
return originalAppendChild.call(this, child);
|
||||||
|
};
|
||||||
|
|
||||||
|
Node.prototype.removeChild = function (child) {
|
||||||
|
debug("dom removeChild", {
|
||||||
|
parent: summarizeNode(this),
|
||||||
|
child: summarizeNode(child)
|
||||||
|
});
|
||||||
|
return originalRemoveChild.call(this, child);
|
||||||
|
};
|
||||||
|
|
||||||
|
Node.prototype.replaceChild = function (newChild, oldChild) {
|
||||||
|
debug("dom replaceChild", {
|
||||||
|
parent: summarizeNode(this),
|
||||||
|
newChild: summarizeNode(newChild),
|
||||||
|
oldChild: summarizeNode(oldChild)
|
||||||
|
});
|
||||||
|
return originalReplaceChild.call(this, newChild, oldChild);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeMutation(mutation) {
|
function summarizeMutation(mutation) {
|
||||||
return {
|
return {
|
||||||
type: mutation.type,
|
type: mutation.type,
|
||||||
@@ -181,6 +237,79 @@ window.rpgRollerApi = (() => {
|
|||||||
logWorkspaceSnapshot(`phase:${label}`);
|
logWorkspaceSnapshot(`phase:${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bootstrapPreBlazorDiagnostics(requestPath) {
|
||||||
|
installGlobalDiagnostics();
|
||||||
|
installDomOperationDiagnostics();
|
||||||
|
|
||||||
|
const host = document.getElementById("rr-interactive-host");
|
||||||
|
workspaceDiagnostics.route = requestPath;
|
||||||
|
debug("bootstrapPreBlazorDiagnostics", {
|
||||||
|
requestPath,
|
||||||
|
readyState: document.readyState,
|
||||||
|
bodyChildren: summarizeChildren(document.body),
|
||||||
|
host: summarizeNode(host),
|
||||||
|
hostChildren: summarizeChildren(host)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
warn("bootstrapPreBlazorDiagnostics missing host", { requestPath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preconnectObserver = new MutationObserver((mutations) => {
|
||||||
|
debug("preblazor host mutations", {
|
||||||
|
requestPath,
|
||||||
|
mutations: mutations.slice(0, 20).map(summarizeMutation),
|
||||||
|
bodyChildren: summarizeChildren(document.body),
|
||||||
|
hostChildren: summarizeChildren(host)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
preconnectObserver.observe(host, {
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
attributes: true,
|
||||||
|
characterData: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyObserver = new MutationObserver((mutations) => {
|
||||||
|
debug("preblazor body mutations", {
|
||||||
|
requestPath,
|
||||||
|
mutations: mutations.slice(0, 20).map(summarizeMutation),
|
||||||
|
bodyChildren: summarizeChildren(document.body),
|
||||||
|
hostChildren: summarizeChildren(host)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bodyObserver.observe(document.body, {
|
||||||
|
subtree: false,
|
||||||
|
childList: true,
|
||||||
|
attributes: false
|
||||||
|
});
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
debug("preblazor microtask snapshot", {
|
||||||
|
requestPath,
|
||||||
|
bodyChildren: summarizeChildren(document.body),
|
||||||
|
hostChildren: summarizeChildren(host)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
debug("preblazor raf snapshot", {
|
||||||
|
requestPath,
|
||||||
|
bodyChildren: summarizeChildren(document.body),
|
||||||
|
hostChildren: summarizeChildren(host)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
debug("preblazor timeout50 snapshot", {
|
||||||
|
requestPath,
|
||||||
|
bodyChildren: summarizeChildren(document.body),
|
||||||
|
hostChildren: summarizeChildren(host)
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
function toAppUrl(url) {
|
function toAppUrl(url) {
|
||||||
if (!url || typeof url !== "string") {
|
if (!url || typeof url !== "string") {
|
||||||
return url;
|
return url;
|
||||||
@@ -588,6 +717,7 @@ window.rpgRollerApi = (() => {
|
|||||||
scrollElementToBottom,
|
scrollElementToBottom,
|
||||||
clearInputValue,
|
clearInputValue,
|
||||||
installWorkspaceDiagnostics,
|
installWorkspaceDiagnostics,
|
||||||
markWorkspacePhase
|
markWorkspacePhase,
|
||||||
|
bootstrapPreBlazorDiagnostics
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
1
TASKS.md
1
TASKS.md
@@ -24,6 +24,7 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
|||||||
- [x] (2026-05-04 21:58Z) Removed shell-level `OnAfterRenderAsync` bootstrapping, moved the JS-dependent authenticated startup into a route-owned `WorkspaceRouteView`, removed shell-owned staged control renders, restored the missing development database fixture, and updated README to describe the completed route-first architecture.
|
- [x] (2026-05-04 21:58Z) Removed shell-level `OnAfterRenderAsync` bootstrapping, moved the JS-dependent authenticated startup into a route-owned `WorkspaceRouteView`, removed shell-owned staged control renders, restored the missing development database fixture, and updated README to describe the completed route-first architecture.
|
||||||
- [x] (2026-05-04) Updated host tests, Selenium smoke tests, and docs so the real-route model is the documented and verified Milestone 2 behavior.
|
- [x] (2026-05-04) Updated host tests, Selenium smoke tests, and docs so the real-route model is the documented and verified Milestone 2 behavior.
|
||||||
- [x] (2026-05-04) Added expanded workspace startup diagnostics across Blazor lifecycle logging, route-content render logging, and browser-side DOM mutation snapshots to narrow the remaining Firefox batch-2 crash.
|
- [x] (2026-05-04) Added expanded workspace startup diagnostics across Blazor lifecycle logging, route-content render logging, and browser-side DOM mutation snapshots to narrow the remaining Firefox batch-2 crash.
|
||||||
|
- [x] (2026-05-04) Extended the diagnostics to page-load time by wrapping the interactive host in a stable container and logging pre-Blazor body and host mutations before the first interactive batch applies.
|
||||||
|
|
||||||
## Surprises & Discoveries
|
## Surprises & Discoveries
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user