Skip to content

Commit 4e9f5e7

Browse files
authored
Merge pull request #8173 from NomicFoundation/refactor-sentry-reporter
Lazy-load Sentry reporter
2 parents c0eb92a + 0af370b commit 4e9f5e7

11 files changed

Lines changed: 159 additions & 84 deletions

File tree

.changeset/cool-planets-reflect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Improve error reporter

packages/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
3636
import { deepEqual } from "@nomicfoundation/hardhat-utils/lang";
3737
import debug from "debug";
3838

39-
import { sendErrorTelemetry } from "../../../cli/telemetry/sentry/reporter.js";
39+
import { sendErrorTelemetry } from "../../../cli/telemetry/error-reporter/reporter.js";
4040
import { EDR_NETWORK_REVERT_SNAPSHOT_EVENT } from "../../../constants.js";
4141
import { hardhatChainTypeToEdrChainType } from "../../../edr/chain-type.js";
4242
import { getGlobalEdrContext } from "../../../edr/context.js";

packages/hardhat/src/internal/builtin-plugins/node/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import chalk from "chalk";
1616
// micro-eth-signer is known to be slow to load, so we lazy load it
1717
let microEthSigner: typeof MicroEthSignerT | undefined;
1818

19-
import { sendErrorTelemetry } from "../../cli/telemetry/sentry/reporter.js";
19+
import { sendErrorTelemetry } from "../../cli/telemetry/error-reporter/reporter.js";
2020
import { isDefaultEdrNetworkHDAccountsConfig } from "../network-manager/edr/edr-provider.js";
2121
import { normalizeEdrNetworkAccountsConfig } from "../network-manager/edr/utils/convert-to-edr.js";
2222

packages/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { TestResult } from "@nomicfoundation/edr";
99
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex";
1010
import chalk from "chalk";
1111

12-
import { sendErrorTelemetry } from "../../cli/telemetry/sentry/reporter.js";
12+
import { sendErrorTelemetry } from "../../cli/telemetry/error-reporter/reporter.js";
1313
import { SolidityTestStackTraceGenerationError } from "../network-manager/edr/stack-traces/stack-trace-generation-errors.js";
1414
import { encodeStackTraceEntry } from "../network-manager/edr/stack-traces/stack-trace-solidity-errors.js";
1515
import { formatTraces } from "../network-manager/edr/utils/trace-formatters.js";

packages/hardhat/src/internal/cli/init/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
} from "../../utils/package.js";
3232
import { BannerManager } from "../banner-manager.js";
3333
import { sendProjectTypeAnalytics } from "../telemetry/analytics/analytics.js";
34-
import { sendErrorTelemetry } from "../telemetry/sentry/reporter.js";
34+
import { sendErrorTelemetry } from "../telemetry/error-reporter/reporter.js";
3535

3636
import {
3737
getDevDependenciesInstallationCommand,

packages/hardhat/src/internal/cli/main.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ import { printErrorMessages } from "./error-handler.js";
4646
import { getGlobalHelpString } from "./help/get-global-help-string.js";
4747
import { getHelpString } from "./help/get-help-string.js";
4848
import { sendTaskAnalytics } from "./telemetry/analytics/analytics.js";
49+
import { setupGlobalUnhandledErrorHandlers } from "./telemetry/error-reporter/global-error-handlers.js";
4950
import {
5051
sendErrorTelemetry,
5152
setCliHardhatConfigPath,
52-
setupErrorTelemetryIfEnabled,
53-
} from "./telemetry/sentry/reporter.js";
53+
} from "./telemetry/error-reporter/reporter.js";
5454
import { printVersionMessage } from "./version.js";
5555

5656
export interface MainOptions {
@@ -64,7 +64,9 @@ export async function main(
6464
rawArguments: string[],
6565
options: MainOptions = {},
6666
): Promise<void> {
67-
await setupErrorTelemetryIfEnabled();
67+
// We set up the global unhandled errors before running any functionality
68+
setupGlobalUnhandledErrorHandlers();
69+
6870
const print = options.print ?? console.log;
6971

7072
const log = debug("hardhat:core:cli:main");

packages/hardhat/src/internal/cli/node-version.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// NOTE: We don't use the `semver` package because it's slow to load, and this
22
// is always run during the initialization of the CLI.
3+
//
4+
// NOTE: This file shouldn't import any non-builtin dependency, as it's imported
5+
// before enabling source maps support. TODO: Change chalk to util.styleText
36

47
import chalk from "chalk";
58

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
2+
import debug from "debug";
3+
4+
import { sendErrorTelemetry } from "./reporter.js";
5+
6+
const log = debug("hardhat:core:telemetry:global-error-handlers");
7+
8+
function createUnhandledErrorListener(isPromiseRejection: boolean) {
9+
const description = isPromiseRejection
10+
? "Unhandled promise rejection"
11+
: "Uncaught exception";
12+
13+
const mechanismType = isPromiseRejection
14+
? "onunhandledrejection"
15+
: "onuncaughtexception";
16+
17+
async function listener(error: Error | unknown) {
18+
log(description, error);
19+
20+
const telemetryError =
21+
error instanceof Error
22+
? error
23+
: new Error(
24+
isObject(error) &&
25+
"message" in error &&
26+
typeof error.message === "string"
27+
? error.message
28+
: "Unknown error",
29+
{ cause: error },
30+
);
31+
32+
try {
33+
await sendErrorTelemetry(telemetryError, {
34+
unhandled: true,
35+
mechanismType,
36+
});
37+
} catch (telemetryErrorReportingError) {
38+
log(
39+
"Failed to send telemetry for unhandled error",
40+
telemetryErrorReportingError,
41+
);
42+
}
43+
44+
console.error();
45+
console.error(`${description}:`);
46+
console.error();
47+
console.error(error);
48+
49+
process.exit(1);
50+
}
51+
52+
return listener;
53+
}
54+
55+
/**
56+
* Sets up global error handlers that report unhandled errors and promise
57+
* rejections if authorized by the user.
58+
*/
59+
export function setupGlobalUnhandledErrorHandlers(): void {
60+
log("Setting up global unhandled error handlers");
61+
62+
process.on("uncaughtException", createUnhandledErrorListener(false));
63+
process.on("unhandledRejection", createUnhandledErrorListener(true));
64+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type * as SentryReporterT from "../sentry/reporter.js";
2+
3+
// Sentry's reporter loads a large number of modules, so we only load it if
4+
// needed.
5+
let sentryReporterModule: typeof SentryReporterT | undefined;
6+
7+
// We cache the `setCliHardhatConfigPath` to avoid loading the reporter just
8+
// for this setting. We load it and set the config path if needed.
9+
let cliHardhatConfigPath: string | undefined;
10+
11+
/**
12+
* Reports an error if telemetry is authorized by the user.
13+
*
14+
* While this function is async, it's expected to always complete quickly,
15+
* delegating the actual reporting to a subprocess.
16+
*
17+
* @param error - The error to report.
18+
* @param hint - Optional metadata describing how the error was captured, used
19+
* to tag the event in Sentry so unhandled crashes can be distinguished from
20+
* errors caught and reported by the CLI.
21+
* @param hint.unhandled - Whether the error reached a global handler without
22+
* being caught (e.g. an uncaught exception or unhandled promise rejection).
23+
* Defaults to `false` (i.e. the error was handled).
24+
* @param hint.mechanismType - The Sentry mechanism type, used as a tag on the
25+
* event. Common values are `"onuncaughtexception"`, `"onunhandledrejection"`,
26+
* `"instrument"`, and `"generic"`. Defaults to `"generic"`.
27+
*/
28+
export async function sendErrorTelemetry(
29+
error: Error,
30+
hint?: { unhandled?: boolean; mechanismType?: string },
31+
): Promise<void> {
32+
if (sentryReporterModule === undefined) {
33+
sentryReporterModule = await import("../sentry/reporter.js");
34+
}
35+
36+
if (cliHardhatConfigPath !== undefined) {
37+
sentryReporterModule.setCliHardhatConfigPath(cliHardhatConfigPath);
38+
}
39+
40+
await sentryReporterModule.sendErrorTelemetry(error, hint);
41+
}
42+
43+
/**
44+
* Sets the config path used in the Hardhat CLI. This is used for better
45+
* anonymization of errors.
46+
*/
47+
export function setCliHardhatConfigPath(configPath: string): void {
48+
cliHardhatConfigPath = configPath;
49+
}
Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
/* This file is inspired by https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/sdk/index.ts */
22

3-
import type {
4-
Client,
5-
ServerRuntimeClientOptions,
6-
Transport,
7-
} from "@sentry/core";
3+
import type { ServerRuntimeClientOptions, Transport } from "@sentry/core";
84

95
import os from "node:os";
106
import path from "node:path";
@@ -18,15 +14,12 @@ import {
1814
ServerRuntimeClient,
1915
stackParserFromStackParserOptions,
2016
} from "@sentry/core";
21-
import debug from "debug";
2217

2318
import { GENERIC_SERVER_NAME } from "./constants.js";
2419
import { nodeContextIntegration } from "./vendor/integrations/context.js";
2520
import { contextLinesIntegration } from "./vendor/integrations/contextlines.js";
2621
import { createGetModuleFromFilename } from "./vendor/utils/module.js";
2722

28-
const log = debug("hardhat:core:sentry:init");
29-
3023
interface GlobalCustomSentryReporterOptions {
3124
/**
3225
* Sentry's DSN
@@ -49,12 +42,6 @@ interface GlobalCustomSentryReporterOptions {
4942
* See the transport module for the different options.
5043
*/
5144
transport: Transport;
52-
53-
/**
54-
* If `true`, the global unhandled rejection and uncaught exception handlers
55-
* will be installed.
56-
*/
57-
installGlobalHandlers?: boolean;
5845
}
5946

6047
/**
@@ -72,12 +59,9 @@ interface GlobalCustomSentryReporterOptions {
7259
* The reason that this uses the global instance of sentry (by calling
7360
* initAndBind), is that using the client directly doesn't work with the linked
7461
* errors integration.
75-
*
76-
* Calling `init` also has an option to set global unhandled rejection and
77-
* uncaught exception handlers.
7862
*/
7963
export function init(options: GlobalCustomSentryReporterOptions): void {
80-
const client = initAndBind<ServerRuntimeClient, ServerRuntimeClientOptions>(
64+
initAndBind<ServerRuntimeClient, ServerRuntimeClientOptions>(
8165
ServerRuntimeClient,
8266
{
8367
dsn: options.dsn,
@@ -113,48 +97,4 @@ export function init(options: GlobalCustomSentryReporterOptions): void {
11397
),
11498
},
11599
);
116-
117-
setupGlobalUnhandledErrorHandlers(client);
118-
}
119-
120-
function createUnhandledErrorListener(
121-
client: Client,
122-
isPromiseRejection: boolean,
123-
) {
124-
const description = isPromiseRejection
125-
? "Unhandled promise rejection"
126-
: "Uncaught exception";
127-
128-
async function listener(error: Error | unknown) {
129-
log(description, error);
130-
131-
client.captureException(error, {
132-
captureContext: {
133-
level: "fatal",
134-
},
135-
mechanism: {
136-
handled: false,
137-
type: "onuncaughtexception",
138-
},
139-
});
140-
141-
await client.flush(100);
142-
await client.close(100);
143-
144-
console.error();
145-
console.error(`${description}:`);
146-
console.error();
147-
console.error(error);
148-
149-
process.exit(1);
150-
}
151-
152-
return listener;
153-
}
154-
155-
function setupGlobalUnhandledErrorHandlers(client: Client) {
156-
log("Setting up global unhandled error handlers");
157-
158-
process.on("uncaughtException", createUnhandledErrorListener(client, false));
159-
process.on("unhandledRejection", createUnhandledErrorListener(client, true));
160100
}

0 commit comments

Comments
 (0)