diff --git a/packages/replay-next/src/suspense/NetworkRequestsCache.ts b/packages/replay-next/src/suspense/NetworkRequestsCache.ts index 69c5844fcba..57abef4e5bf 100644 --- a/packages/replay-next/src/suspense/NetworkRequestsCache.ts +++ b/packages/replay-next/src/suspense/NetworkRequestsCache.ts @@ -19,8 +19,9 @@ import { import { StreamingCacheLoadOptions, createStreamingCache } from "suspense"; import { comparePoints } from "protocol/execution-point-utils"; +import { transformSupplementalId } from "protocol/utils"; import { assert } from "protocol/utils"; -import { ReplayClientInterface } from "shared/client/types"; +import { ReplayClientInterface, TargetPoint } from "shared/client/types"; export type NetworkEventWithTime = EventType & { time: number; @@ -41,6 +42,7 @@ export type NetworkRequestsData = { }; timeStampedPoint: TimeStampedPoint; triggerPoint: TimeStampedPoint | null; + targetPoint: TargetPoint | null; }; export type NetworkRequestsCacheData = { @@ -57,7 +59,7 @@ export const networkRequestsCache = createStreamingCache< getKey: () => "single-entry-cache", load: async ( options: StreamingCacheLoadOptions>, - replayClient + replayClient: ReplayClientInterface ) => { const { update, resolve } = options; @@ -79,6 +81,8 @@ export const networkRequestsCache = createStreamingCache< ids.push(id); + const targetPoint = replayClient.getTargetPoint(point, 0); + records[id] = { id, events: { @@ -98,6 +102,7 @@ export const networkRequestsCache = createStreamingCache< point, time, }, + targetPoint: targetPoint ?? null, triggerPoint: triggerPoint ?? null, } as NetworkRequestsData; }); diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 3dc23555669..f2300f4a4ce 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -90,8 +90,9 @@ import { ReplayClientEvents, ReplayClientInterface, SourceLocationRange, - SupplementalSession, SupplementalRecordingConnection, + SupplementalSession, + TargetPoint, TimeStampedPointWithPaintHash, } from "./types"; @@ -502,47 +503,70 @@ export class ReplayClient implements ReplayClientInterface { } async breakdownSupplementalLocation(location: Location) { - const { id: sourceId, sessionId, supplementalIndex } = await this.breakdownSupplementalIdAndSession(location.sourceId); + const { + id: sourceId, + sessionId, + supplementalIndex, + } = await this.breakdownSupplementalIdAndSession(location.sourceId); return { location: { ...location, sourceId }, sessionId, supplementalIndex }; } async breakdownSupplementalPointSelector(pointSelector: PointSelector) { switch (pointSelector.kind) { case "location": { - const { location, sessionId, supplementalIndex } = await this.breakdownSupplementalLocation(pointSelector.location); + const { location, sessionId, supplementalIndex } = await this.breakdownSupplementalLocation( + pointSelector.location + ); return { pointSelector: { ...pointSelector, location }, sessionId, supplementalIndex }; } case "locations": { let commonSessionId: string | undefined; let commonSupplementalIndex = 0; - const locations = await Promise.all(pointSelector.locations.map(async transformedLocation => { - const { location, sessionId, supplementalIndex } = await this.breakdownSupplementalLocation(transformedLocation); - if (commonSessionId) { - assert(commonSessionId == sessionId); - } else { - commonSessionId = sessionId; - commonSupplementalIndex = supplementalIndex; - } - return location; - })); + const locations = await Promise.all( + pointSelector.locations.map(async transformedLocation => { + const { location, sessionId, supplementalIndex } = + await this.breakdownSupplementalLocation(transformedLocation); + if (commonSessionId) { + assert(commonSessionId == sessionId); + } else { + commonSessionId = sessionId; + commonSupplementalIndex = supplementalIndex; + } + return location; + }) + ); assert(commonSessionId); - return { pointSelector: { ...pointSelector, locations }, sessionId: commonSessionId, supplementalIndex: commonSupplementalIndex }; + return { + pointSelector: { ...pointSelector, locations }, + sessionId: commonSessionId, + supplementalIndex: commonSupplementalIndex, + }; } case "points": { let commonSessionId: string | undefined; let commonSupplementalIndex = 0; - const points = await Promise.all(pointSelector.points.map(async transformedPoint => { - const { id: point, sessionId, supplementalIndex } = await this.breakdownSupplementalIdAndSession(transformedPoint); - if (commonSessionId) { - assert(commonSessionId == sessionId); - } else { - commonSessionId = sessionId; - commonSupplementalIndex = supplementalIndex; - } - return point; - })); + const points = await Promise.all( + pointSelector.points.map(async transformedPoint => { + const { + id: point, + sessionId, + supplementalIndex, + } = await this.breakdownSupplementalIdAndSession(transformedPoint); + if (commonSessionId) { + assert(commonSessionId == sessionId); + } else { + commonSessionId = sessionId; + commonSupplementalIndex = supplementalIndex; + } + return point; + }) + ); assert(commonSessionId); - return { pointSelector: { ...pointSelector, points }, sessionId: commonSessionId, supplementalIndex: commonSupplementalIndex }; + return { + pointSelector: { ...pointSelector, points }, + sessionId: commonSessionId, + supplementalIndex: commonSupplementalIndex, + }; } default: return { pointSelector, sessionId: await this.waitForSession(), supplementalIndex: 0 }; @@ -611,22 +635,24 @@ export class ReplayClient implements ReplayClientInterface { }); } - private async maybeGetConnectionStepTarget(point: ExecutionPoint, pointSupplementalIndex: number): Promise { + getTargetPoint(point: ExecutionPoint, pointSupplementalIndex: number): { + point: TimeStampedPoint, supplementalIndex: number + } | null { const recordingId = this.getSupplementalIndexRecordingId(pointSupplementalIndex); - let targetPoint: ExecutionPoint | undefined; + let targetPoint: TimeStampedPoint | undefined; let targetSupplementalIndex = 0; this.forAllConnections((serverRecordingId, connection, supplementalIndex) => { const { clientFirst, clientRecordingId, clientPoint, serverPoint } = connection; if (clientFirst) { if (clientRecordingId == recordingId && clientPoint.point == point) { - targetPoint = serverPoint.point; + targetPoint = serverPoint; targetSupplementalIndex = supplementalIndex; } } else { if (serverRecordingId == recordingId && serverPoint.point == point) { assert(clientRecordingId == this._recordingId, "NYI"); - targetPoint = clientPoint.point; + targetPoint = clientPoint; targetSupplementalIndex = 0; } } @@ -636,11 +662,21 @@ export class ReplayClient implements ReplayClientInterface { return null; } - const sessionId = await this.getSupplementalIndexSession(targetSupplementalIndex); + return { point: targetPoint, supplementalIndex: targetSupplementalIndex }; + } + + private async maybeGetConnectionStepTarget(point: ExecutionPoint, pointSupplementalIndex: number): Promise { + + const targetPoint = this.getTargetPoint(point, pointSupplementalIndex); + if (!targetPoint) { + return null; + } + + const sessionId = await this.getSupplementalIndexSession(targetPoint.supplementalIndex); - const response = await sendMessage("Session.getPointFrameSteps" as any, { point: targetPoint }, sessionId); + const response = await sendMessage("Session.getPointFrameSteps" as any, { point: targetPoint.point }, sessionId); const { steps } = response; - const desc = steps.find((step: PointDescription) => step.point == targetPoint); + const desc = steps.find((step: PointDescription) => step.point == targetPoint.point?.point); assert(desc); this.transformSupplementalPointDescription(desc, sessionId); @@ -1542,7 +1578,7 @@ function interpolateSupplementalTime(recordingId: string, supplemental: Suppleme assert(previous.clientPoint.time <= next.clientPoint.time); assert(previous.serverPoint.time <= next.serverPoint.time); if (supplementalTime >= previous.serverPoint.time && - supplementalTime <= next.serverPoint.time) { + supplementalTime <= next.serverPoint.time) { const clientElapsed = next.clientPoint.time - previous.clientPoint.time; const serverElapsed = next.serverPoint.time - previous.serverPoint.time; const fraction = (supplementalTime - previous.serverPoint.time) / serverElapsed; diff --git a/packages/shared/client/types.ts b/packages/shared/client/types.ts index c09594a8181..d74738ca764 100644 --- a/packages/shared/client/types.ts +++ b/packages/shared/client/types.ts @@ -178,10 +178,19 @@ export interface SupplementalSession extends SupplementalRecording { sessionId: string; } +export interface TargetPoint { + point: TimeStampedPoint; + supplementalIndex: number; +} + export interface ReplayClientInterface { get loadedRegions(): LoadedRegions | null; addEventListener(type: ReplayClientEvents, handler: Function): void; - configure(recordingId: string, sessionId: string, supplemental: SupplementalSession[]): Promise; + configure( + recordingId: string, + sessionId: string, + supplemental: SupplementalSession[] + ): Promise; createPause(executionPoint: ExecutionPoint): Promise; evaluateExpression( pauseId: PauseId, @@ -207,6 +216,7 @@ export interface ReplayClientInterface { events: RequestEventInfo[]; requests: RequestInfo[]; }>; + getTargetPoint(point: ExecutionPoint, pointSupplementalIndex: number): TargetPoint | null; findPaints(): Promise; findPoints(selector: PointSelector, limits?: PointLimits): Promise; diff --git a/src/stories/NetworkMonitor/utils.ts b/src/stories/NetworkMonitor/utils.ts index e7732f036df..79282860212 100644 --- a/src/stories/NetworkMonitor/utils.ts +++ b/src/stories/NetworkMonitor/utils.ts @@ -96,6 +96,7 @@ export const requestSummary = ( name: "replay.io", path: getPathFromUrl(url), point: { point: "0", time: 0 }, + targetPoint: null, queryParams: [["foo", "bar"]], triggerPoint: { point: "0", time: 0 }, requestHeaders: [{ name: "foo", value: "bar" }], diff --git a/src/ui/components/NetworkMonitor/NetworkMonitorListRow.module.css b/src/ui/components/NetworkMonitor/NetworkMonitorListRow.module.css index b9920ef4bdd..89ab706cbea 100644 --- a/src/ui/components/NetworkMonitor/NetworkMonitorListRow.module.css +++ b/src/ui/components/NetworkMonitor/NetworkMonitorListRow.module.css @@ -89,6 +89,7 @@ color: #fff; font-weight: bold; } + .Row:hover .SeekButton { display: flex; } @@ -97,6 +98,45 @@ outline: none; } +.ServerSeekButton { + display: none; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + + flex-direction: row; + align-items: center; + padding: 0; + gap: 2px; + + background: var(--body-bgcolor); + color: #fff; + font-weight: bold; +} + +.Row:hover .ServerSeekButton { + display: flex; +} + +.ServerSeekJumpButton { + background: var(--background-color-primary-button); + gap: 1ch; + padding: 0.25rem 1ch; +} + +.ServerSeekJumpButton:hover { + background-color: var(--background-color-primary-button-hover); +} + +.Server:focus, +.Server:hover { + outline: none; +} + .SeekButtonIcon { flex: 0 0 auto; color: currentColor; diff --git a/src/ui/components/NetworkMonitor/NetworkMonitorListRow.tsx b/src/ui/components/NetworkMonitor/NetworkMonitorListRow.tsx index f060471a420..51cfce9a15e 100644 --- a/src/ui/components/NetworkMonitor/NetworkMonitorListRow.tsx +++ b/src/ui/components/NetworkMonitor/NetworkMonitorListRow.tsx @@ -1,14 +1,18 @@ +import { TimeStampedPoint } from "@replayio/protocol"; import { CSSProperties, MouseEvent, useContext, useEffect, useState } from "react"; +import { transformSupplementalId } from "protocol/utils"; import Icon from "replay-next/components/Icon"; import { SessionContext } from "replay-next/src/contexts/SessionContext"; import { useNag } from "replay-next/src/hooks/useNag"; import { networkRequestBodyCache } from "replay-next/src/suspense/NetworkRequestsCache"; import { ReplayClientContext } from "shared/client/ReplayClientContext"; import { Nag } from "shared/graphql/types"; +import { seek } from "ui/actions/timeline"; import useNetworkContextMenu from "ui/components/NetworkMonitor/useNetworkContextMenu"; import { EnabledColumns } from "ui/components/NetworkMonitor/useNetworkMonitorColumns"; import { RequestSummary, findHeader } from "ui/components/NetworkMonitor/utils"; +import { useAppDispatch } from "ui/setup/hooks"; import { BodyPartsToUInt8Array, @@ -29,7 +33,7 @@ export type ItemData = { filteredBeforeCount: number; firstRequestIdAfterCurrentTime: string | null; requests: RequestSummary[]; - seekToRequest: (row: RequestSummary) => void; + seekToRequest: (request: RequestSummary) => void; selectRequest: (row: RequestSummary | null) => void; selectedRequestId: string | null; }; @@ -116,9 +120,14 @@ function RequestRow({ start: startTime, status, triggerPoint, + targetPoint, url, } = request; + if (targetPoint) { + console.log("targetPoint!!", targetPoint); + } + let type = documentType || cause; if (type === "unknown") { type = ""; @@ -143,6 +152,8 @@ function RequestRow({ } } + const dispatch = useAppDispatch(); + const [, dismissJumpToNetworkRequestNag] = useNag(Nag.JUMP_TO_NETWORK_REQUEST); useEffect(() => { @@ -254,7 +265,45 @@ function RequestRow({ )} - {triggerPoint && triggerPoint.time !== currentTime && ( + {targetPoint && ( +
+ + +
+ )} + + {!targetPoint && triggerPoint && triggerPoint.time !== currentTime && (