Skip to content

Commit

Permalink
Show reason for crashed recording
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Jun 5, 2024
1 parent 9a1b043 commit 1d35869
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 39 deletions.
64 changes: 39 additions & 25 deletions packages/replayio/src/commands/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import { exitProcess } from "../utils/exitProcess";
import { killProcess } from "../utils/killProcess";
import { trackEvent } from "../utils/mixpanel/trackEvent";
import { canUpload } from "../utils/recordings/canUpload";
import { getRecordingUnusableReason } from "../utils/recordings/getRecordingUnusableReason";
import { getRecordings } from "../utils/recordings/getRecordings";
import { printRecordings } from "../utils/recordings/printRecordings";
import { selectRecordings } from "../utils/recordings/selectRecordings";
import { LocalRecording } from "../utils/recordings/types";
import { uploadRecordings } from "../utils/recordings/upload/uploadRecordings";
import { dim } from "../utils/theme";
import { dim, statusFailed } from "../utils/theme";

registerCommand("record", { checkForRuntimeUpdate: true, requireAuthentication: true })
.argument("[url]", `URL to open (default: "about:blank")`)
Expand Down Expand Up @@ -55,39 +56,45 @@ async function record(url: string = "about:blank") {
} catch (error) {
if (error instanceof ProcessError) {
const { errorLogPath, uploaded } = await reportBrowserCrash(error.stderr);

console.log("\nSomething went wrong while recording. Try again.");
console.log(dim(`More information can be found in ${errorLogPath}`));
console.log(dim(`\nMore information can be found in ${errorLogPath}`));
if (uploaded) {
console.log(dim(`The crash was reported to the Replay team`));
}

await exitProcess(1);
}
}

const recordingsAfter = getRecordings(processGroupId);
const crashedRecordings: LocalRecording[] = [];
const finishedRecordings: LocalRecording[] = [];
const unusableRecordings: LocalRecording[] = [];

const nextCrashedRecordings: LocalRecording[] = [];
const nextRecordings: LocalRecording[] = [];

recordingsAfter.filter(recording => {
if (recording.recordingStatus === "crashed") {
if (canUpload(recording)) {
nextCrashedRecordings.push(recording);
}
} else {
nextRecordings.push(recording);
getRecordings(processGroupId).forEach(recording => {
switch (recording.recordingStatus) {
case "crashed":
if (canUpload(recording)) {
crashedRecordings.push(recording);
}
break;
case "unusable":
unusableRecordings.push(recording);
break;
default:
finishedRecordings.push(recording);
}
});

console.log(""); // Spacing for readability

// First check for any new crashes; these we should upload automatically
if (nextCrashedRecordings.length > 0) {
if (crashedRecordings.length > 0) {
console.log(
"It looks like something went wrong with this recording. Please hold while we upload crash data."
"It looks like something went wrong while recording. Please hold while we upload crash data."
);

const promise = uploadRecordings(nextCrashedRecordings, {
const promise = uploadRecordings(crashedRecordings, {
processingBehavior: "do-not-process",
silent: true,
});
Expand All @@ -102,11 +109,18 @@ async function record(url: string = "about:blank") {
}

console.log(""); // Spacing for readability
} else if (unusableRecordings.length > 0) {
// If there were unusable recordings we should provide explicit messaging about them
const reason = getRecordingUnusableReason(processGroupId);
if (reason) {
console.log("An error occurred while recording:\n" + statusFailed(reason));
console.log(""); // Spacing for readability
}
}

trackEvent("record.results", {
crashedCount: nextCrashedRecordings.length,
successCountsByType: nextRecordings.reduce(
crashedCount: crashedRecordings.length,
successCountsByType: finishedRecordings.reduce(
(map, recording) => {
const processType = recording.metadata.processType ?? "unknown";
map[processType] ??= 0;
Expand All @@ -125,32 +139,32 @@ async function record(url: string = "about:blank") {
});

// Then let the user decide what to do with the other new recordings
if (nextRecordings.length > 0) {
if (finishedRecordings.length > 0) {
if (!process.stdin.isTTY) {
console.log(
"New recording(s) found:\n" +
printRecordings(nextRecordings, {
printRecordings(finishedRecordings, {
showHeaderRow: false,
})
);
} else {
let selectedRecordings: LocalRecording[] = [];
if (nextRecordings.length === 1) {
if (finishedRecordings.length === 1) {
const confirmed = await confirm(
"New recording found. Would you like to upload it?",
true,
"\n" +
printRecordings(nextRecordings, {
printRecordings(finishedRecordings, {
showHeaderRow: false,
})
);
if (confirmed) {
selectedRecordings = nextRecordings;
selectedRecordings = finishedRecordings;
}

console.log(""); // Spacing for readability
} else {
selectedRecordings = await selectRecordings(nextRecordings, {
selectedRecordings = await selectRecordings(finishedRecordings, {
defaultSelected: recording => recording.metadata.processType === "root",
prompt: "New recordings found. Which would you like to upload?",
selectionMessage: "The following recording(s) will be uploaded:",
Expand All @@ -161,7 +175,7 @@ async function record(url: string = "about:blank") {
await uploadRecordings(selectedRecordings, { processingBehavior: "start-processing" });
}
}
} else if (nextCrashedRecordings.length === 0) {
} else if (crashedRecordings.length === 0) {
// It doesn't make sense to print this message if there were crashes
console.log("No new recordings were created");
}
Expand Down
1 change: 0 additions & 1 deletion packages/replayio/src/commands/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ async function upload(

selectedRecordings = await selectRecordings(recordings, {
defaultSelected: recording => recording.metadata.processType === "root",
disabledSelector: recording => !canUpload(recording),
noSelectableRecordingsMessage:
"The recording(s) below cannot be uploaded.\n" +
printRecordings(recordings, { showHeaderRow: false }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getRecordings } from "./getRecordings";

export function getRecordingUnusableReason(processGroupIdFilter?: string) {
// Look for the most recent unusable recording; that is most likely to be related
return getRecordings(processGroupIdFilter).findLast(
recording => recording.recordingStatus === "unusable" && recording.unusableReason
)?.unusableReason;
}
28 changes: 18 additions & 10 deletions packages/replayio/src/utils/recordings/getRecordings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function getRecordings(processGroupIdFilter?: string): LocalRecording[] {
path: undefined,
processingStatus: undefined,
recordingStatus: "recording",
unusableReason: undefined,
uploadStatus: undefined,
};

Expand Down Expand Up @@ -176,14 +177,12 @@ export function getRecordings(processGroupIdFilter?: string): LocalRecording[] {
break;
}
case RECORDING_LOG_KIND.recordingUnusable: {
const { id } = entry;
const { id, reason } = entry;
const recording = idToRecording[id];

assert(recording, `Recording with ID "${id}" not found`);
recording.recordingStatus = "unusable";

const index = recordings.indexOf(recording);
recordings.splice(index, 1);
recording.unusableReason = reason;
break;
}
case RECORDING_LOG_KIND.sourcemapAdded: {
Expand Down Expand Up @@ -265,7 +264,7 @@ export function getRecordings(processGroupIdFilter?: string): LocalRecording[] {
}
}

debug("Found %s recordings:\n%o", recordings.length, recordings);
debug("Found %s recordings:\n%o\n%o", recordings.length, JSON.stringify(recordings, null, 2));

return (
recordings
Expand All @@ -274,11 +273,20 @@ export function getRecordings(processGroupIdFilter?: string): LocalRecording[] {
return false;
}

if (!recording.metadata.host) {
// Ignore new/empty tab recordings (see TT-1036)
// Note that we filter all "empty" recordings, not just root recordings,
// because Chrome loads its default new tab content via an <iframe>
return false;
switch (recording.recordingStatus) {
case "finished":
if (!recording.metadata.host) {
// Ignore new/empty tab recordings (see TT-1036)
//
// Note that we filter all "empty" recordings, not just root recordings,
// because Chrome loads its default new tab content via an <iframe>
//
// Also note that we only remove finished recordings in this way,
// because it's important to report unusable or crashed recordings to the user
// Those may have crashed before host metadata was added
return false;
}
break;
}

return true;
Expand Down
17 changes: 14 additions & 3 deletions packages/replayio/src/utils/recordings/selectRecordings.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
// @ts-ignore TS types are busted; see github.com/enquirer/enquirer/issues/212
import { MultiSelect } from "bvaughn-enquirer";
import { dim, select, transparent } from "../theme";
import { canUpload } from "./canUpload";
import { printRecordings } from "./printRecordings";
import { LocalRecording } from "./types";

export async function selectRecordings(
recordings: LocalRecording[],
options: {
defaultSelected?: (recording: LocalRecording) => boolean;
disabledSelector?: (recording: LocalRecording) => boolean;
maxRecordingsToDisplay?: number;
noSelectableRecordingsMessage?: string;
prompt: string;
selectionMessage: string;
}
): Promise<LocalRecording[]> {
const {
defaultSelected = () => true,
disabledSelector = () => false,
defaultSelected: defaultSelectedProp,
maxRecordingsToDisplay = 25,
noSelectableRecordingsMessage,
prompt,
selectionMessage,
} = options;

const defaultSelected = (recording: LocalRecording) => {
if (!canUpload(recording)) {
return false;
} else if (defaultSelectedProp) {
return defaultSelectedProp(recording);
} else {
return true;
}
};

const disabledSelector = (recording: LocalRecording) => !canUpload(recording);

let isShowingAllRecordings = true;
if (maxRecordingsToDisplay != null && recordings.length > maxRecordingsToDisplay) {
isShowingAllRecordings = false;
Expand Down
2 changes: 2 additions & 0 deletions packages/replayio/src/utils/recordings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type LogEntry = {
parentId?: string;
parentOffset?: number;
path?: string;
reason?: string;
recordingId?: string;
server?: string;
targetContentHash?: string;
Expand Down Expand Up @@ -84,5 +85,6 @@ export type LocalRecording = {
path: string | undefined;
processingStatus: "failed" | "processed" | "processing" | undefined;
recordingStatus: "crashed" | "finished" | "recording" | "unusable";
unusableReason: string | undefined;
uploadStatus: "failed" | "uploading" | "uploaded" | undefined;
};

0 comments on commit 1d35869

Please sign in to comment.