Add detailed logging to bottom anchor controller and scroll strategy

This commit is contained in:
Mohamed Boudra
2026-03-08 20:48:37 +07:00
parent 87948e956a
commit 7c6eb2ad74
3 changed files with 132 additions and 8 deletions
@@ -0,0 +1,30 @@
import { defineConfig, devices } from "@playwright/test";
const baseURL =
process.env.E2E_BASE_URL ??
`http://localhost:${process.env.E2E_METRO_PORT ?? "8081"}`;
export default defineConfig({
testDir: "./e2e",
globalSetup: "./e2e/global-setup.ts",
timeout: 60_000,
expect: {
timeout: 10_000,
},
fullyParallel: false,
workers: 1,
retries: 0,
reporter: [["list"]],
use: {
baseURL,
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{
name: "Desktop Safari",
use: { ...devices["Desktop Safari"] },
},
],
});
@@ -245,6 +245,32 @@ function forceScrollContainerToBottom(
} }
} }
function logWebBottomAnchorTransport(
event: string,
details: Record<string, unknown>
): void {
if (!(globalThis as { __DEV__?: boolean }).__DEV__) {
return;
}
console.debug("[BottomAnchorTransport]", event, details);
}
function getResolvedWebScrollContainerMetrics(
refs: StreamRenderRefs
): Record<string, unknown> {
const scrollNode = resolveWebScrollContainerNode(refs);
if (!scrollNode) {
return { hasScrollNode: false };
}
return {
hasScrollNode: true,
scrollTop: scrollNode.scrollTop,
scrollHeight: scrollNode.scrollHeight,
clientHeight: scrollNode.clientHeight,
tagName: scrollNode.tagName,
};
}
function createStreamRenderStrategy( function createStreamRenderStrategy(
config: StreamRenderStrategyConfig config: StreamRenderStrategyConfig
): StreamRenderStrategy { ): StreamRenderStrategy {
@@ -376,12 +402,28 @@ function createForwardStreamStrategy(): StreamRenderStrategy {
0, 0,
metrics.contentHeight - metrics.viewportHeight metrics.contentHeight - metrics.viewportHeight
); );
logWebBottomAnchorTransport("forward_scroll_to_bottom_start", {
animated,
bottomOffset,
metrics,
usedFlatList: Boolean(refs.flatListRef.current),
...getResolvedWebScrollContainerMetrics(refs),
});
if (refs.flatListRef.current) { if (refs.flatListRef.current) {
refs.flatListRef.current.scrollToEnd?.({ animated }); refs.flatListRef.current.scrollToEnd?.({ animated });
refs.flatListRef.current.scrollToOffset?.({ refs.flatListRef.current.scrollToOffset?.({
offset: bottomOffset, offset: bottomOffset,
animated, animated,
}); });
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
logWebBottomAnchorTransport("forward_scroll_to_bottom_after_flatlist", {
animated,
bottomOffset,
...getResolvedWebScrollContainerMetrics(refs),
});
});
}
return; return;
} }
const usedAnchor = scrollAnchorIntoView({ refs, animated }); const usedAnchor = scrollAnchorIntoView({ refs, animated });
@@ -394,6 +436,16 @@ function createForwardStreamStrategy(): StreamRenderStrategy {
animated, animated,
}); });
forceScrollContainerToBottom(refs, bottomOffset); forceScrollContainerToBottom(refs, bottomOffset);
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
logWebBottomAnchorTransport("forward_scroll_to_bottom_after_scrollview", {
animated,
bottomOffset,
usedAnchor,
...getResolvedWebScrollContainerMetrics(refs),
});
});
}
}, },
scrollToOffset: ({ refs, offset, animated }) => { scrollToOffset: ({ refs, offset, animated }) => {
if (refs.flatListRef.current) { if (refs.flatListRef.current) {
@@ -233,6 +233,20 @@ function deriveRetryDisposition(input: {
: "retry-scroll"; : "retry-scroll";
} }
function getDetailedMeasurementState(
measurementState: ControllerMeasurementState
): Record<string, unknown> {
return {
containerKey: measurementState.containerKey,
viewportWidth: measurementState.viewportWidth,
viewportHeight: measurementState.viewportHeight,
contentHeight: measurementState.contentHeight,
offsetY: measurementState.offsetY,
viewportMeasuredForKey: measurementState.viewportMeasuredForKey,
contentMeasuredForKey: measurementState.contentMeasuredForKey,
};
}
function createBottomAnchorControllerDriver( function createBottomAnchorControllerDriver(
input: CreateBottomAnchorControllerDriverInput input: CreateBottomAnchorControllerDriverInput
): BottomAnchorControllerDriver { ): BottomAnchorControllerDriver {
@@ -344,18 +358,37 @@ function createBottomAnchorControllerDriver(
}); });
if (verificationBlockedReason) { if (verificationBlockedReason) {
input.log(
"attempt_verified",
getLogContext({
verificationPhase: "blocked",
verificationBlockedReason,
retries: attemptContext.retries,
measurementState: getDetailedMeasurementState(measurementState),
})
);
pendingVerification = attemptContext; pendingVerification = attemptContext;
setBlockedReason(verificationBlockedReason); setBlockedReason(verificationBlockedReason);
return; return;
} }
const verifiedNearBottom = input.isNearBottom(); const verifiedNearBottom = input.isNearBottom();
const retryDisposition = verifiedNearBottom
? null
: deriveRetryDisposition({
mode,
retries: attemptContext.retries,
verificationRetryMode:
input.getTransportBehavior().verificationRetryMode,
});
input.log( input.log(
"attempt_verified", "attempt_verified",
getLogContext({ getLogContext({
verifiedNearBottom, verifiedNearBottom,
retries: attemptContext.retries, retries: attemptContext.retries,
retryDisposition,
measurementState: getDetailedMeasurementState(measurementState),
}) })
); );
@@ -370,12 +403,6 @@ function createBottomAnchorControllerDriver(
return; return;
} }
const retryDisposition = deriveRetryDisposition({
mode,
retries: attemptContext.retries,
verificationRetryMode: input.getTransportBehavior().verificationRetryMode,
});
if (retryDisposition === "retry-verify") { if (retryDisposition === "retry-verify") {
pendingVerification = { pendingVerification = {
requestId: attemptContext.requestId, requestId: attemptContext.requestId,
@@ -397,7 +424,11 @@ function createBottomAnchorControllerDriver(
input.log( input.log(
"attempt_failed", "attempt_failed",
getLogContext({ retries: attemptContext.retries }) getLogContext({
retries: attemptContext.retries,
retryDisposition,
measurementState: getDetailedMeasurementState(measurementState),
})
); );
pendingVerification = null; pendingVerification = null;
if (isRequestAttempt && currentRequest) { if (isRequestAttempt && currentRequest) {
@@ -443,10 +474,11 @@ function createBottomAnchorControllerDriver(
kind: "attempt", kind: "attempt",
callback: () => { callback: () => {
attemptHandle = null; attemptHandle = null;
const measurementState = input.getMeasurementState();
const nextBlockedReason = deriveBottomAnchorBlockedReason({ const nextBlockedReason = deriveBottomAnchorBlockedReason({
pendingRequest, pendingRequest,
isAuthoritativeHistoryReady: input.getIsAuthoritativeHistoryReady(), isAuthoritativeHistoryReady: input.getIsAuthoritativeHistoryReady(),
measurementState: input.getMeasurementState(), measurementState,
pendingVerificationRequestId: pendingVerification?.requestId ?? null, pendingVerificationRequestId: pendingVerification?.requestId ?? null,
}); });
setBlockedReason(nextBlockedReason); setBlockedReason(nextBlockedReason);
@@ -462,6 +494,16 @@ function createBottomAnchorControllerDriver(
!shouldAttemptForPendingRequest && !shouldAttemptForPendingRequest &&
!shouldAttemptForStickyVerification !shouldAttemptForStickyVerification
) { ) {
input.log(
"attempt_started",
getLogContext({
attemptPhase: "skipped",
nextBlockedReason,
shouldAttemptForPendingRequest,
shouldAttemptForStickyVerification,
measurementState: getDetailedMeasurementState(measurementState),
})
);
return; return;
} }