-
Notifications
You must be signed in to change notification settings - Fork 968
fix(site): update useAgentLogs
to make it more testable and add more tests
#19126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Parkreiner
wants to merge
39
commits into
main
Choose a base branch
from
mes/logs-flake
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
d6e00c3
wip: commit progress on test update
Parkreiner 43d0ca8
refactor: update useAgentLogs tests as unit tests
Parkreiner 727bddd
docs: rewrite comment for clarity
Parkreiner d46d144
fix: remove unnecessary type
Parkreiner 91a6fc1
fix: make sure logs have different timestamps
Parkreiner ecbe7b0
fix: add different dates to reduce risk of false positives
Parkreiner d13bcdc
Merge branch 'main' into mes/logs-flake
Parkreiner 0f21097
Merge branch 'main' into mes/logs-flake
Parkreiner abd6553
refactor: decrease coupling
Parkreiner bade97a
wip: commit progress on updating flake
Parkreiner e11fefd
Merge branch 'mes/logs-flake' of https://github.com/coder/coder into …
Parkreiner bc3d095
fix: get all tests passing
Parkreiner 550d09e
chore: add one more test case
Parkreiner cc7e632
fix: update type mismatches
Parkreiner 79c7ffd
refactor: clean up some code
Parkreiner 43a0d3a
fix: make testing boundaries more formal
Parkreiner 982d3e1
fix: remove premature optimization
Parkreiner 41c5a12
fix: update setup
Parkreiner 42cb73b
fix: update state sync logic
Parkreiner 3a5f7bb
Merge branch 'main' into mes/logs-flake
Parkreiner 35a40df
fix: update wonky types
Parkreiner 306dbc7
Merge branch 'main' into mes/logs-flake
Parkreiner f49e55a
Merge branch 'main' into mes/logs-flake
Parkreiner c2fc772
fix: update tests
Parkreiner 2cabd85
fix: format
Parkreiner 855f3ca
Merge branch 'main' into mes/logs-flake
Parkreiner 453894b
fix: apply initial feedback
Parkreiner c9f2b12
wip: commit refactoring progress
Parkreiner 80865fe
refactor: update assignment
Parkreiner f930b29
wip: prepare to change indents
Parkreiner a311ac9
fix: update keygen logic
Parkreiner 5657536
chore: add basic overflow message
Parkreiner 6547a2f
chore: swap to tailwind
Parkreiner c818aec
wip: commit progress
Parkreiner 30bb008
Merge branch 'main' into mes/logs-flake
Parkreiner dac3828
refactor: switch to spy setup
Parkreiner 7a013d3
fix: get scaffolding for new AgentLogs in place
Parkreiner b71d051
fix: remove bad import
Parkreiner 092c4e5
fix: knip
Parkreiner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
refactor: switch to spy setup
- Loading branch information
commit dac38281e572293b752bfab06c7db554fca6e4ce
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,97 +1,73 @@ | ||
import { | ||
type WatchWorkspaceAgentLogsParams, | ||
watchWorkspaceAgentLogs, | ||
} from "api/api"; | ||
import { watchWorkspaceAgentLogs } from "api/api"; | ||
import type { WorkspaceAgentLog } from "api/typesGenerated"; | ||
import { displayError } from "components/GlobalSnackbar/utils"; | ||
import { useEffect, useState } from "react"; | ||
import type { OneWayWebSocket } from "utils/OneWayWebSocket"; | ||
|
||
export type CreateUseAgentLogsOptions = Readonly<{ | ||
onError: (errorEvent: Event) => void; | ||
createSocket: ( | ||
agentId: string, | ||
params?: WatchWorkspaceAgentLogsParams, | ||
) => OneWayWebSocket<WorkspaceAgentLog[]>; | ||
}>; | ||
|
||
type UseAgentLogsOptions = Readonly<{ | ||
agentId: string; | ||
enabled?: boolean; | ||
}>; | ||
|
||
export function createUseAgentLogs(createOptions: CreateUseAgentLogsOptions) { | ||
const { createSocket, onError } = createOptions; | ||
export function useAgentLogs( | ||
options: UseAgentLogsOptions, | ||
): readonly WorkspaceAgentLog[] { | ||
const { agentId, enabled = true } = options; | ||
const [logs, setLogs] = useState<readonly WorkspaceAgentLog[]>([]); | ||
|
||
return function useAgentLogs( | ||
options: UseAgentLogsOptions, | ||
): readonly WorkspaceAgentLog[] { | ||
const { agentId, enabled = true } = options; | ||
const [logs, setLogs] = useState<readonly WorkspaceAgentLog[]>([]); | ||
// Clean up the logs when the agent is not enabled, using a mid-render | ||
// sync to remove any risk of screen flickering. Clearing the logs helps | ||
// ensure that if the hook flips back to being enabled, we can receive a | ||
// fresh set of logs from the beginning with zero risk of duplicates. | ||
const [prevEnabled, setPrevEnabled] = useState(enabled); | ||
if (!enabled && prevEnabled) { | ||
setLogs([]); | ||
setPrevEnabled(false); | ||
} | ||
if (enabled && !prevEnabled) { | ||
setPrevEnabled(true); | ||
} | ||
|
||
// Clean up the logs when the agent is not enabled, using a mid-render | ||
// sync to remove any risk of screen flickering. Clearing the logs helps | ||
// ensure that if the hook flips back to being enabled, we can receive a | ||
// fresh set of logs from the beginning with zero risk of duplicates. | ||
const [prevEnabled, setPrevEnabled] = useState(enabled); | ||
if (!enabled && prevEnabled) { | ||
setLogs([]); | ||
setPrevEnabled(false); | ||
} | ||
if (enabled && !prevEnabled) { | ||
setPrevEnabled(true); | ||
useEffect(() => { | ||
if (!enabled) { | ||
return; | ||
} | ||
|
||
useEffect(() => { | ||
if (!enabled) { | ||
// Always fetch the logs from the beginning. We may want to optimize | ||
// this in the future, but it would add some complexity in the code | ||
// that might not be worth it. | ||
const socket = watchWorkspaceAgentLogs(agentId, { after: 0 }); | ||
socket.addEventListener("message", (e) => { | ||
if (e.parseError) { | ||
console.warn("Error parsing agent log: ", e.parseError); | ||
return; | ||
} | ||
|
||
// Always fetch the logs from the beginning. We may want to optimize | ||
// this in the future, but it would add some complexity in the code | ||
// that might not be worth it. | ||
const socket = createSocket(agentId, { after: 0 }); | ||
socket.addEventListener("message", (e) => { | ||
if (e.parseError) { | ||
console.warn("Error parsing agent log: ", e.parseError); | ||
return; | ||
} | ||
|
||
if (e.parsedMessage.length === 0) { | ||
return; | ||
} | ||
if (e.parsedMessage.length === 0) { | ||
return; | ||
} | ||
|
||
setLogs((logs) => { | ||
const newLogs = [...logs, ...e.parsedMessage]; | ||
newLogs.sort((l1, l2) => { | ||
const d1 = new Date(l1.created_at).getTime(); | ||
const d2 = new Date(l2.created_at).getTime(); | ||
return d1 - d2; | ||
}); | ||
return newLogs; | ||
setLogs((logs) => { | ||
const newLogs = [...logs, ...e.parsedMessage]; | ||
newLogs.sort((l1, l2) => { | ||
const d1 = new Date(l1.created_at).getTime(); | ||
const d2 = new Date(l2.created_at).getTime(); | ||
return d1 - d2; | ||
}); | ||
return newLogs; | ||
}); | ||
}); | ||
|
||
socket.addEventListener("error", (e) => { | ||
onError(e); | ||
socket.close(); | ||
}); | ||
socket.addEventListener("error", (e) => { | ||
console.error("Error in agent log socket: ", e); | ||
displayError( | ||
"Unable to watch agent logs", | ||
"Please try refreshing the browser", | ||
); | ||
socket.close(); | ||
}); | ||
|
||
return () => socket.close(); | ||
}, [agentId, enabled]); | ||
return () => socket.close(); | ||
}, [agentId, enabled]); | ||
|
||
return logs; | ||
}; | ||
return logs; | ||
} | ||
|
||
// The baseline implementation to use for production | ||
export const useAgentLogs = createUseAgentLogs({ | ||
createSocket: watchWorkspaceAgentLogs, | ||
onError: (errorEvent) => { | ||
console.error("Error in agent log socket: ", errorEvent); | ||
displayError( | ||
"Unable to watch agent logs", | ||
"Please try refreshing the browser", | ||
); | ||
}, | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we stick with this we could just use
createRef
andReact.RefObject<MockWebSocketServer>
instead of making our own type hereThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are not the same.
createRef
is for interfacing with class components, and creates a mutable object that's still part of React's data flowThis setup is ejecting a value created during React's lifecycle into an outside function. React has absolutely no idea that this exists, but it's how a lot of React testing tools work under the hood. I get the aversion to having more bespoke types, but switching to the built-in type would be lying about it actually is