Skip to content

Commit 240fb19

Browse files
authored
Merge pull request #7134 from NomicFoundation/refactor-sentry-reporter
Refactor sentry reporter
2 parents a7f9950 + e3dcac8 commit 240fb19

File tree

18 files changed

+477
-796
lines changed

18 files changed

+477
-796
lines changed

v-next/hardhat/src/cli.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,5 @@ function isTsxRequired(): boolean {
2323
return true;
2424
}
2525

26-
main(process.argv.slice(2), { registerTsx: isTsxRequired() }).catch(() => {
27-
process.exitCode = 1;
28-
});
26+
// eslint-disable-next-line no-restricted-syntax -- We do want TLA here
27+
await main(process.argv.slice(2), { registerTsx: isTsxRequired() });

v-next/hardhat/src/internal/cli/main.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ import { printErrorMessages } from "./error-handler.js";
3737
import { getGlobalHelpString } from "./help/get-global-help-string.js";
3838
import { getHelpString } from "./help/get-help-string.js";
3939
import { sendTaskAnalytics } from "./telemetry/analytics/analytics.js";
40-
import { sendErrorTelemetry } from "./telemetry/sentry/reporter.js";
40+
import {
41+
sendErrorTelemetry,
42+
setCliHardhatConfigPath,
43+
setupErrorTelemetryIfEnabled,
44+
} from "./telemetry/sentry/reporter.js";
4145
import { printVersionMessage } from "./version.js";
4246

4347
export interface MainOptions {
@@ -50,6 +54,7 @@ export async function main(
5054
rawArguments: string[],
5155
options: MainOptions = {},
5256
): Promise<void> {
57+
await setupErrorTelemetryIfEnabled();
5358
const print = options.print ?? console.log;
5459

5560
const log = debug("hardhat:core:cli:main");
@@ -86,6 +91,8 @@ export async function main(
8691
builtinGlobalOptions.configPath,
8792
);
8893

94+
setCliHardhatConfigPath(configPath);
95+
8996
const projectRoot = await resolveProjectRoot(configPath);
9097

9198
const esmErrorPrinted = await printEsmErrorMessageIfNecessary(
@@ -198,7 +205,7 @@ export async function main(
198205

199206
if (error instanceof Error) {
200207
try {
201-
await sendErrorTelemetry(error, configPath);
208+
await sendErrorTelemetry(error);
202209
} catch (e) {
203210
log("Couldn't report error to sentry: %O", e);
204211
}

v-next/hardhat/src/internal/cli/telemetry/sentry/anonymizer.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { Event, Exception, StackFrame, Stacktrace } from "@sentry/core";
1+
import type {
2+
Envelope,
3+
Event,
4+
EventItem,
5+
Exception,
6+
StackFrame,
7+
Stacktrace,
8+
} from "@sentry/core";
29

310
import * as path from "node:path";
411

@@ -18,12 +25,18 @@ import * as simplifiedChinese from "ethereum-cryptography/bip39/wordlists/simpli
1825
import * as SPANISH from "ethereum-cryptography/bip39/wordlists/spanish.js";
1926
import * as traditionalChinese from "ethereum-cryptography/bip39/wordlists/traditional-chinese.js";
2027

28+
import { GENERIC_SERVER_NAME } from "./constants.js";
29+
2130
interface WordMatch {
2231
index: number;
2332
word: string;
2433
}
2534

26-
export type AnonymizeResult =
35+
export type AnonymizeEnvelopeResult =
36+
| { success: true; envelope: Envelope }
37+
| { success: false; error: string };
38+
39+
export type AnonymizeEventResult =
2740
| { success: true; event: Event }
2841
| { success: false; error: string };
2942

@@ -39,25 +52,47 @@ export class Anonymizer {
3952
this.#configPath = configPath;
4053
}
4154

55+
/**
56+
* Anonymizes the events in the envelope in place, modifying the envelope.
57+
*/
58+
public async anonymizeEventsFromEnvelope(
59+
envelope: Envelope,
60+
): Promise<AnonymizeEnvelopeResult> {
61+
for (const item of envelope[1]) {
62+
if (item[0].type === "event") {
63+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
64+
-- We know that the item is an event item */
65+
const eventItem = item as EventItem;
66+
67+
const anonymizedEvent = await this.anonymizeEvent(eventItem[1]);
68+
if (anonymizedEvent.success) {
69+
eventItem[1] = anonymizedEvent.event;
70+
} else {
71+
return { success: false, error: anonymizedEvent.error };
72+
}
73+
}
74+
}
75+
76+
return { success: true, envelope };
77+
}
78+
4279
/**
4380
* Given a sentry serialized exception
4481
* (https://develop.sentry.dev/sdk/event-payloads/exception/), return an
4582
* anonymized version of the event.
4683
*/
47-
public async anonymize(event: any): Promise<AnonymizeResult> {
48-
if (event === null || event === undefined) {
49-
return { success: false, error: "event is null or undefined" };
50-
}
51-
if (typeof event !== "object") {
52-
return { success: false, error: "event is not an object" };
53-
}
54-
84+
public async anonymizeEvent(event: Event): Promise<AnonymizeEventResult> {
5585
const result: Event = {
5686
event_id: event.event_id,
5787
platform: event.platform,
58-
release: event.release,
5988
timestamp: event.timestamp,
6089
extra: event.extra,
90+
release: event.release,
91+
contexts: event.contexts,
92+
sdk: event.sdk,
93+
level: event.level,
94+
server_name: GENERIC_SERVER_NAME,
95+
environment: event.environment,
6196
};
6297

6398
if (event.exception !== undefined && event.exception.values !== undefined) {
@@ -244,6 +279,7 @@ export class Anonymizer {
244279
async #anonymizeException(value: Exception): Promise<Exception> {
245280
const result: Exception = {
246281
type: value.type,
282+
mechanism: value.mechanism,
247283
};
248284

249285
if (value.value !== undefined) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const GENERIC_SERVER_NAME = "<server-name>";
Lines changed: 115 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,146 @@
11
/* This file is inspired by https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/sdk/index.ts */
22

33
import type {
4-
BaseTransportOptions,
5-
Integration,
4+
Client,
65
ServerRuntimeClientOptions,
76
Transport,
87
} from "@sentry/core";
98

9+
import os from "node:os";
10+
import path from "node:path";
11+
1012
import {
1113
createStackParser,
1214
functionToStringIntegration,
13-
getIntegrationsToSetup,
1415
initAndBind,
1516
linkedErrorsIntegration,
1617
nodeStackLineParser,
17-
requestDataIntegration,
1818
ServerRuntimeClient,
1919
stackParserFromStackParserOptions,
2020
} from "@sentry/core";
21+
import debug from "debug";
2122

22-
import { getHardhatVersion } from "../../../utils/package.js";
23-
24-
import { onUncaughtExceptionIntegration } from "./integrations/onuncaughtexception.js";
25-
import { onUnhandledRejectionIntegration } from "./integrations/onunhandledrejection.js";
23+
import { GENERIC_SERVER_NAME } from "./constants.js";
2624
import { nodeContextIntegration } from "./vendor/integrations/context.js";
2725
import { contextLinesIntegration } from "./vendor/integrations/contextlines.js";
2826
import { createGetModuleFromFilename } from "./vendor/utils/module.js";
2927

30-
interface InitOptions {
28+
const log = debug("hardhat:core:sentry:init");
29+
30+
interface GlobalCustomSentryReporterOptions {
31+
/**
32+
* Sentry's DSN
33+
*/
3134
dsn: string;
32-
transport: (transportOptions: BaseTransportOptions) => Transport;
33-
release?: string;
34-
serverName?: string;
35-
integrations?: (integrations: Integration[]) => Integration[];
35+
36+
/**
37+
* The environment used to report the events
38+
*/
39+
environment: string;
40+
41+
/**
42+
* The release of Hardhat
43+
*/
44+
release: string;
45+
46+
/**
47+
* A transport that customizes how we send envelopes to Sentry's server.
48+
*
49+
* See the transport module for the different options.
50+
*/
51+
transport: Transport;
52+
53+
/**
54+
* If `true`, the global unhandled rejection and uncaught exception handlers
55+
* will be installed.
56+
*/
57+
installGlobalHandlers?: boolean;
3658
}
3759

3860
/**
39-
* Initialize Sentry for Node, without performance instrumentation.
61+
* This function initializes a custom global sentry reporter/client.
62+
*
63+
* There are two reasons why we customize it, instead of using the default one
64+
* provided by @sentry/node:
65+
* - @sentry/node has an astronomical amount of dependencies -- See https://github.com/getsentry/sentry-javascript/discussions/13846
66+
* - We customize the transport to avoid blocking the main Hardhat process
67+
* while reporting errors.
68+
*
69+
* Once you initialize the custom global sentry reporter, you can use the usual
70+
* `captureException` and `captureMessage` functions exposed by @sentry/core.
71+
*
72+
* The reason that this uses the global instance of sentry (by calling
73+
* initAndBind), is that using the client directly doesn't work with the linked
74+
* errors integration.
75+
*
76+
* Calling `init` also has an option to set global unhandled rejection and
77+
* uncaught exception handlers.
4078
*/
41-
export async function init(options: InitOptions): Promise<void> {
42-
const stackParser = stackParserFromStackParserOptions(
43-
createStackParser(nodeStackLineParser(createGetModuleFromFilename())),
79+
export function init(options: GlobalCustomSentryReporterOptions): void {
80+
const client = initAndBind<ServerRuntimeClient, ServerRuntimeClientOptions>(
81+
ServerRuntimeClient,
82+
{
83+
dsn: options.dsn,
84+
environment: options.environment,
85+
serverName: GENERIC_SERVER_NAME,
86+
release: options.release,
87+
initialScope: {
88+
contexts: {
89+
os: {
90+
name: os.type(),
91+
build: os.release(),
92+
version: os.version(),
93+
},
94+
device: {
95+
arch: os.arch(),
96+
},
97+
runtime: {
98+
name: path.basename(process.title),
99+
version: process.version,
100+
},
101+
},
102+
},
103+
transport: () => options.transport,
104+
integrations: [
105+
functionToStringIntegration(),
106+
contextLinesIntegration(),
107+
linkedErrorsIntegration(),
108+
nodeContextIntegration(),
109+
],
110+
platform: process.platform,
111+
stackParser: stackParserFromStackParserOptions(
112+
createStackParser(nodeStackLineParser(createGetModuleFromFilename())),
113+
),
114+
},
44115
);
45116

46-
// NOTE: We do not include most of the default integrations @sentry/node does
47-
// because in the main hardhat process, we don't use the default integrations
48-
// at all, and they're of limited use in the context of a reporter subprocess.
49-
const integrationOptions = {
50-
defaultIntegrations: [
51-
// Inbound filters integration filters out events (errors and transactions) mainly based on init inputs we never use
52-
// Import from @sentry/core if needed
53-
// inboundFiltersIntegration(),
54-
55-
functionToStringIntegration(),
56-
linkedErrorsIntegration(),
57-
requestDataIntegration(),
58-
59-
// Native Wrappers
60-
// Console integration captures console logs as breadcrumbs
61-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/console.ts if needed
62-
// consoleIntegration(),
63-
64-
// HTTP integration instruments the http(s) modules to capture outgoing requests and attach them as breadcrumbs/spans
65-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/http/index.ts if needed
66-
// httpIntegration(),
67-
68-
// Native Node Fetch integration instruments the native node fetch module to capture outgoing requests and attach them as breadcrumbs/spans
69-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/node-fetch/index.ts if needed
70-
// nativeNodeFetchIntegration(),
71-
72-
// Global Handlers
73-
onUncaughtExceptionIntegration(),
74-
onUnhandledRejectionIntegration(),
75-
76-
// Event Info
77-
contextLinesIntegration(),
78-
nodeContextIntegration(),
79-
80-
// Local variables integrations adds local variables to exception frames
81-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/local-variables/local-variables-async.ts if needed
82-
// localVariablesIntegration(),
83-
84-
// Child process integration captures child process/worker thread events as breadcrumbs
85-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/childProcess.ts if needed
86-
// childProcessIntegration(),
87-
88-
// Records a session for the current process to track release health
89-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/processSession.ts if needed
90-
// processSessionIntegration(),
91-
92-
// CommonJS Only
93-
// Vendor https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/modules.ts if needed
94-
// modulesIntegration(),
95-
],
96-
integrations: options.integrations,
97-
};
98-
99-
const clientOptions: ServerRuntimeClientOptions = {
100-
sendClientReports: true,
101-
...options,
102-
platform: "node",
103-
runtime: {
104-
name: "node",
105-
version: process.version,
106-
},
107-
stackParser,
108-
integrations: getIntegrationsToSetup(integrationOptions),
109-
_metadata: {
110-
sdk: {
111-
name: "hardhat",
112-
version: await getHardhatVersion(),
117+
setupGlobalUnhandledErrorHandlers(client);
118+
}
119+
120+
function setupGlobalUnhandledErrorHandlers(client: Client) {
121+
log("Setting up global unhandled error handlers");
122+
123+
async function listener(error: Error | unknown) {
124+
log("Uncaught exception", error);
125+
126+
client.captureException(error, {
127+
captureContext: {
128+
level: "fatal",
113129
},
114-
},
115-
};
130+
mechanism: {
131+
handled: false,
132+
type: "onuncaughtexception",
133+
},
134+
});
135+
136+
await client.flush(100);
137+
await client.close(100);
138+
139+
console.error("Unexpected error encountered:\n");
140+
console.error(error);
141+
process.exit(1);
142+
}
116143

117-
initAndBind(ServerRuntimeClient, clientOptions);
144+
process.on("uncaughtException", listener);
145+
process.on("unhandledRejection", listener);
118146
}

0 commit comments

Comments
 (0)