Skip to content

Commit

Permalink
Upload data related to browser crashes (#503)
Browse files Browse the repository at this point in the history
* Upload data related to browser crashes

* switch to `os.homedir`

* cleanup reportBrowserCrash

* merge iterations in `findMostRecentFile`
  • Loading branch information
Andarist authored Jun 5, 2024
1 parent a6d7ddc commit 4954cd1
Show file tree
Hide file tree
Showing 18 changed files with 167 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-books-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"replayio": patch
---

Added automatic browser crash reporting to the Replay team
3 changes: 1 addition & 2 deletions packages/replayio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@
"launchdarkly-node-client-sdk": "^3.2.1",
"log-update": "^4",
"mixpanel": "^0.18.0",
"node-fetch": "^2.6.8",
"open": "^8.4.2",
"pretty-ms": "^7.0.1",
"query-registry": "^2.6.0",
"semver": "^7.5.4",
"superstruct": "^0.15.4",
"table": "^6.8.2",
"undici": "^5.28.4",
"ws": "^7.5.0"
},
"devDependencies": {
Expand All @@ -63,7 +63,6 @@
"@types/fs-extra": "latest",
"@types/inquirer": "^9",
"@types/jest": "^28.1.5",
"@types/node-fetch": "^2.6.3",
"@types/table": "^6.3.2",
"@types/ws": "^8.5.10",
"jest": "^28.1.3",
Expand Down
18 changes: 5 additions & 13 deletions packages/replayio/src/commands/record.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import debug from "debug";
import { writeFileSync } from "fs-extra";
import { v4 as uuid } from "uuid";
import { ProcessError } from "../utils/ProcessError";
import { logAsyncOperation } from "../utils/async/logAsyncOperation";
import { getRunningProcess } from "../utils/browser/getRunningProcess";
import { launchBrowser } from "../utils/browser/launchBrowser";
import { reportBrowserCrash } from "../utils/browser/reportBrowserCrash";
import { registerCommand } from "../utils/commander/registerCommand";
import { confirm } from "../utils/confirm";
import { exitProcess } from "../utils/exitProcess";
import { getReplayPath } from "../utils/getReplayPath";
import { killProcess } from "../utils/killProcess";
import { trackEvent } from "../utils/mixpanel/trackEvent";
import { canUpload } from "../utils/recordings/canUpload";
Expand Down Expand Up @@ -55,19 +54,12 @@ async function record(url: string = "about:blank") {
await launchBrowser(url, { processGroupId });
} catch (error) {
if (error instanceof ProcessError) {
const { errorLogPath, uploaded } = await reportBrowserCrash(error.stderr);
console.log("\nSomething went wrong while recording. Try again.");

// TODO [PRO-235] Upload recorder crash data somewhere

const { stderr } = error;
if (stderr.length > 0) {
const errorLogPath = getReplayPath("recorder-crash.log");

writeFileSync(errorLogPath, stderr, "utf8");

console.log(dim(`More information can be found in ${errorLogPath}`));
console.log(dim(`More information can be found in ${errorLogPath}`));
if (uploaded) {
console.log(dim(`The crash was reported to the Replay team`));
}

await exitProcess(1);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fetch from "node-fetch";
import { fetch } from "undici";
import { AuthenticationError } from "./AuthenticationError";
import { authClientId, authHost } from "./config";
import { debug } from "./debug";
Expand Down
73 changes: 73 additions & 0 deletions packages/replayio/src/utils/browser/reportBrowserCrash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { readFile, writeFileSync } from "fs-extra";
import { fetch, File, FormData } from "undici";
import { replayApiServer } from "../../config";
import { findMostRecentFile } from "../../utils/findMostRecentFile";
import { getReplayPath } from "../../utils/getReplayPath";
import { getUserAgent } from "../../utils/getUserAgent";
import { checkAuthentication } from "../../utils/initialization/checkAuthentication";
import { getCurrentRuntimeMetadata } from "../../utils/initialization/getCurrentRuntimeMetadata";
import { runtimeMetadata } from "../../utils/installation/config";
import { debug } from "./debug";

export async function reportBrowserCrash(stderr: string) {
const errorLogPath = getReplayPath("recorder-crash.log");
writeFileSync(errorLogPath, stderr, "utf8");

const accessToken = await checkAuthentication();

if (!accessToken) {
return {
errorLogPath,
uploaded: false,
};
}

const userAgent = getUserAgent();

const formData = new FormData();

formData.set("buildId", getCurrentRuntimeMetadata("chromium")?.buildId ?? "unknown");
formData.set("createdAt", new Date().toISOString());
formData.set("userAgent", userAgent);

formData.append(
"log",
new File([await readFile(errorLogPath)], "log.txt", { type: "text/plain" })
);

const latestCrashpad =
runtimeMetadata.crashpadDirectory &&
(await findMostRecentFile(runtimeMetadata.crashpadDirectory, fileName =>
fileName.endsWith(".dmp")
));
if (latestCrashpad) {
formData.append(
"crashpad",
new File([await readFile(latestCrashpad)], "crash.dmp", { type: "application/octet-stream" })
);
}

try {
const response = await fetch(`${replayApiServer}/v1/browser-crash`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"User-Agent": userAgent,
},
body: formData,
});
if (response.status >= 200 && response.status < 300) {
return {
errorLogPath,
uploaded: true,
};
}
} catch (err) {
debug("Crash data failed to be uploaded: %o", err);
}

return {
errorLogPath,
uploaded: false,
};
}
38 changes: 38 additions & 0 deletions packages/replayio/src/utils/findMostRecentFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { readdir, stat } from "fs-extra";
import { join } from "path";

export async function findMostRecentFile(
directory: string,
predicate: (fileName: string) => boolean = () => true
) {
let mostRecent = undefined as string | undefined;
let mostRecentMtimeMs = 0;

await Promise.all(
(
await readdir(directory)
).map(async fileName => {
const filePath = join(directory, fileName);
let stats;

try {
stats = await stat(filePath);
} catch {
return;
}

if (
!stats.isFile() ||
!predicate(fileName) ||
(mostRecent && stats.mtimeMs < mostRecentMtimeMs)
) {
return;
}

mostRecent = filePath;
mostRecentMtimeMs = stats.mtimeMs;
})
);

return mostRecent;
}
7 changes: 2 additions & 5 deletions packages/replayio/src/utils/getReplayPath.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import assert from "assert";
import { homedir } from "os";
import { join, resolve } from "path";

export function getReplayPath(...path: string[]) {
let basePath;
if (process.env.RECORD_REPLAY_DIRECTORY) {
basePath = process.env.RECORD_REPLAY_DIRECTORY;
} else {
const homeDirectory = process.env.HOME || process.env.USERPROFILE;
assert(homeDirectory, "HOME or USERPROFILE environment variable must be set");

basePath = join(homeDirectory, ".replay");
basePath = join(homedir(), ".replay");
}

return resolve(join(basePath, ...path));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ export async function fetchUserIdFromGraphQLOrThrow(accessToken: string) {
}
}
`,
{
key: accessToken,
},
{},
accessToken
);

Expand Down
2 changes: 1 addition & 1 deletion packages/replayio/src/utils/graphql/queryGraphQL.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fetch from "node-fetch";
import { fetch } from "undici";
import { replayApiServer } from "../../config";
import { getUserAgent } from "../getUserAgent";
import { debug } from "./debug";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fetch } from "undici";
import { version as currentVersion, name as packageName } from "../../../package.json";
import { withTrackAsyncEvent } from "../mixpanel/withTrackAsyncEvent";
import { shouldPrompt } from "../prompt/shouldPrompt";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { existsSync } from "fs-extra";
import { join } from "path";
import { runtimeMetadata, runtimePath } from "../installation/config";
import { getBrowserPath } from "../browser/getBrowserPath";
import { getLatestRelease } from "../installation/getLatestReleases";
import { Release } from "../installation/types";
import { withTrackAsyncEvent } from "../mixpanel/withTrackAsyncEvent";
Expand Down Expand Up @@ -38,9 +37,7 @@ export const checkForRuntimeUpdate = withTrackAsyncEvent(
};
}

const { path: executablePath } = runtimeMetadata;
const runtimeExecutablePath = join(runtimePath, ...executablePath);
if (!existsSync(runtimeExecutablePath)) {
if (!existsSync(getBrowserPath())) {
return {
hasUpdate: true,
fromVersion: undefined,
Expand Down
13 changes: 13 additions & 0 deletions packages/replayio/src/utils/installation/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { homedir } from "os";
import { join } from "path";
import { getReplayPath } from "../getReplayPath";
import { emphasize } from "../theme";
import { Architecture, Platform, Runtime } from "./types";

type Metadata = {
architecture: Architecture;
crashpadDirectory: string | undefined;
destinationName: string;
downloadFileName: string;
path: string[];
Expand All @@ -20,6 +23,14 @@ switch (process.platform) {
case "darwin":
runtimeMetadata = {
architecture,
crashpadDirectory: join(
homedir(),
"Library",
"Application Support",
"Chromium",
"Crashpad",
"pending"
),
destinationName: "Replay-Chromium.app",
downloadFileName:
process.env.RECORD_REPLAY_CHROMIUM_DOWNLOAD_FILE ||
Expand All @@ -35,6 +46,7 @@ switch (process.platform) {
case "linux":
runtimeMetadata = {
architecture,
crashpadDirectory: undefined,
destinationName: "chrome-linux",
downloadFileName:
process.env.RECORD_REPLAY_CHROMIUM_DOWNLOAD_FILE || "linux-replay-chromium.tar.xz",
Expand All @@ -49,6 +61,7 @@ switch (process.platform) {
// Force override for Replay internal testing purposes
runtimeMetadata = {
architecture,
crashpadDirectory: undefined,
destinationName: "replay-chromium",
downloadFileName:
process.env.RECORD_REPLAY_CHROMIUM_DOWNLOAD_FILE || "windows-replay-chromium.zip",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fetch from "node-fetch";
import { mocked } from "jest-mock";
import { getLatestRelease } from "./getLatestReleases";
import { fetch } from "undici";
import { replayAppHost } from "../../config";
import { getLatestRelease } from "./getLatestReleases";
import { Release } from "./types";

jest.mock("node-fetch");
jest.mock("undici");
const mockedFetch = mocked(fetch, true);

jest.mock("./config", () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "assert";
import fetch from "node-fetch";
import { fetch } from "undici";
import { replayAppHost } from "../../config";
import { runtimeMetadata } from "./config";
import { debug } from "./debug";
Expand Down
13 changes: 0 additions & 13 deletions packages/replayio/src/utils/protocol/getKeepAliveAgent.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from "fs";
import fetch, { RequestInit } from "node-fetch";
import { create, defaulted, number, object, optional, Struct } from "superstruct";
import { fetch, RequestInit } from "undici";
import { createLog } from "../../../createLog";
import { UnstructuredMetadata } from "../../types";
import { envString } from "./env";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "assert";
import { ReadStream, createReadStream, stat } from "fs-extra";
import fetch from "node-fetch";
import { fetch } from "undici";
import { replayWsServer } from "../../../config";
import { createDeferred } from "../../async/createDeferred";
import { createPromiseQueue } from "../../async/createPromiseQueue";
Expand All @@ -13,7 +13,6 @@ import { endRecordingMultipartUpload } from "../../protocol/api/endRecordingMult
import { endRecordingUpload } from "../../protocol/api/endRecordingUpload";
import { processRecording } from "../../protocol/api/processRecording";
import { setRecordingMetadata } from "../../protocol/api/setRecordingMetadata";
import { getKeepAliveAgent } from "../../protocol/getKeepAliveAgent";
import { multiPartChunkSize, multiPartMinSizeThreshold } from "../config";
import { debug } from "../debug";
import { LocalRecording, RECORDING_LOG_KIND } from "../types";
Expand Down Expand Up @@ -292,14 +291,14 @@ async function uploadRecordingReadStream(
try {
const response = await Promise.race([
fetch(url, {
agent: getKeepAliveAgent,
headers: {
"Content-Length": size.toString(),
"User-Agent": getUserAgent(),
Connection: "keep-alive",
},
method: "PUT",
body: stream,
duplex: "half",
signal: abortSignal,
}),
streamError.promise,
Expand Down
Loading

0 comments on commit 4954cd1

Please sign in to comment.