hardhat 3.4.3 → 3.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.d.ts +1 -1
  3. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.d.ts.map +1 -1
  4. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.js +9 -9
  5. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.js.map +1 -1
  6. package/dist/src/internal/cli/telemetry/error-classification/classifier.d.ts +58 -0
  7. package/dist/src/internal/cli/telemetry/error-classification/classifier.d.ts.map +1 -0
  8. package/dist/src/internal/cli/telemetry/error-classification/classifier.js +402 -0
  9. package/dist/src/internal/cli/telemetry/error-classification/classifier.js.map +1 -0
  10. package/dist/src/internal/cli/telemetry/error-classification/codebase-dependent-helpers.d.ts +67 -0
  11. package/dist/src/internal/cli/telemetry/error-classification/codebase-dependent-helpers.d.ts.map +1 -0
  12. package/dist/src/internal/cli/telemetry/error-classification/codebase-dependent-helpers.js +140 -0
  13. package/dist/src/internal/cli/telemetry/error-classification/codebase-dependent-helpers.js.map +1 -0
  14. package/dist/src/internal/cli/telemetry/error-classification/filter.d.ts +15 -0
  15. package/dist/src/internal/cli/telemetry/error-classification/filter.d.ts.map +1 -0
  16. package/dist/src/internal/cli/telemetry/error-classification/filter.js +114 -0
  17. package/dist/src/internal/cli/telemetry/error-classification/filter.js.map +1 -0
  18. package/dist/src/internal/cli/telemetry/error-classification/helpers.d.ts +52 -0
  19. package/dist/src/internal/cli/telemetry/error-classification/helpers.d.ts.map +1 -0
  20. package/dist/src/internal/cli/telemetry/error-classification/helpers.js +163 -0
  21. package/dist/src/internal/cli/telemetry/error-classification/helpers.js.map +1 -0
  22. package/dist/src/internal/cli/telemetry/error-reporter/reporter.d.ts.map +1 -1
  23. package/dist/src/internal/cli/telemetry/error-reporter/reporter.js +14 -0
  24. package/dist/src/internal/cli/telemetry/error-reporter/reporter.js.map +1 -1
  25. package/dist/src/internal/cli/telemetry/sentry/anonymizer.d.ts +0 -2
  26. package/dist/src/internal/cli/telemetry/sentry/anonymizer.d.ts.map +1 -1
  27. package/dist/src/internal/cli/telemetry/sentry/anonymizer.js +0 -117
  28. package/dist/src/internal/cli/telemetry/sentry/anonymizer.js.map +1 -1
  29. package/dist/src/internal/cli/telemetry/sentry/init.d.ts.map +1 -1
  30. package/dist/src/internal/cli/telemetry/sentry/init.js +14 -9
  31. package/dist/src/internal/cli/telemetry/sentry/init.js.map +1 -1
  32. package/dist/src/internal/cli/telemetry/sentry/reporter.d.ts.map +1 -1
  33. package/dist/src/internal/cli/telemetry/sentry/reporter.js +1 -27
  34. package/dist/src/internal/cli/telemetry/sentry/reporter.js.map +1 -1
  35. package/dist/src/internal/cli/telemetry/sentry/subprocess.js +11 -17
  36. package/dist/src/internal/cli/telemetry/sentry/subprocess.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/internal/builtin-plugins/coverage/coverage-manager.ts +14 -10
  39. package/src/internal/cli/telemetry/error-classification/classifier.ts +636 -0
  40. package/src/internal/cli/telemetry/error-classification/codebase-dependent-helpers.ts +200 -0
  41. package/src/internal/cli/telemetry/error-classification/filter.ts +140 -0
  42. package/src/internal/cli/telemetry/error-classification/helpers.ts +235 -0
  43. package/src/internal/cli/telemetry/error-reporter/reporter.ts +21 -0
  44. package/src/internal/cli/telemetry/sentry/anonymizer.ts +0 -168
  45. package/src/internal/cli/telemetry/sentry/init.ts +42 -33
  46. package/src/internal/cli/telemetry/sentry/reporter.ts +1 -44
  47. package/src/internal/cli/telemetry/sentry/subprocess.ts +11 -19
  48. package/templates/hardhat-3/01-node-test-runner-viem/package.json +2 -2
  49. package/templates/hardhat-3/02-mocha-ethers/package.json +1 -1
  50. package/templates/hardhat-3/03-minimal/package.json +1 -1
@@ -0,0 +1,200 @@
1
+ /**
2
+ * @file This file has a set of helpers that depend on the codebase. They
3
+ * inspect the stack frames to look for known folders, files, or function names.
4
+ *
5
+ * As such, they are somewhat fragile and need to be periodically reevaluated,
6
+ * especially after large refactors to Hardhat's core.
7
+ */
8
+
9
+ import { type StackFrame, includesAny } from "./helpers.js";
10
+
11
+ /**
12
+ * Returns true when this package is being executed from the Hardhat monorepo
13
+ * source tree instead of from an installed `node_modules/hardhat` package.
14
+ */
15
+ export function isRunningInsideHardhatMonorepo(): boolean {
16
+ // If this file is in `/packages/hardhat/`, as opposed to
17
+ // `node_modules/hardhat/`, then we're running inside the monorepo.
18
+ return import.meta.url.includes("/packages/hardhat/");
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Boundary frame predicates
23
+ //
24
+ // These identify the first-party frames that mark the boundary between
25
+ // Hardhat-controlled code and the user/plugin code that ultimately ran. Both
26
+ // the classifier (to assign a category) and the filter (to find the frame
27
+ // above the boundary) need to agree on what those frames look like, so they
28
+ // share a single definition here.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * Matches the Hardhat frame that imports the user's config file.
33
+ */
34
+ export function isConfigLoadingBoundaryFrame(frame: StackFrame): boolean {
35
+ return (
36
+ frame.location.includes("internal/config-loading.") &&
37
+ frame.functionName?.includes("importUserConfig") === true
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Matches the builtin console task frame that evaluates user input.
43
+ */
44
+ export function isConsoleEvaluationBoundaryFrame(frame: StackFrame): boolean {
45
+ return (
46
+ frame.location.includes("/internal/builtin-plugins/console/task-action.") &&
47
+ frame.functionName?.includes("consoleAction") === true
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Matches the builtin run task frame that executes a user script.
53
+ */
54
+ export function isScriptExecutionBoundaryFrame(frame: StackFrame): boolean {
55
+ return (
56
+ frame.location.includes("/internal/builtin-plugins/run/task-action.") &&
57
+ frame.functionName?.includes("runScriptWithHardhat") === true
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Matches the node:test runner task frame that executes user tests.
63
+ */
64
+ export function isNodeTestExecutionBoundaryFrame(frame: StackFrame): boolean {
65
+ return (
66
+ frame.location.includes("/hardhat-node-test-runner/src/task-action.") &&
67
+ frame.functionName?.includes("testWithHardhat") === true
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Matches the Mocha runner task frame that executes user tests.
73
+ */
74
+ export function isMochaTestExecutionBoundaryFrame(frame: StackFrame): boolean {
75
+ return (
76
+ frame.location.includes("/hardhat-mocha/src/task-action.") &&
77
+ frame.functionName?.includes("testWithHardhat") === true
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Matches the resolved-task frame that calls into a task action.
83
+ */
84
+ export function isTaskActionBoundaryFrame(frame: StackFrame): boolean {
85
+ return (
86
+ frame.location.includes("/internal/core/tasks/resolved-task.") &&
87
+ frame.functionName?.includes(".run") === true
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Matches the hook-manager frame that calls into hook handlers.
93
+ */
94
+ export function isHookHandlerBoundaryFrame(frame: StackFrame): boolean {
95
+ return (
96
+ frame.location.includes("/internal/core/hook-manager.") &&
97
+ includesAny(
98
+ frame.functionName,
99
+ ".runHandlerChain",
100
+ ".runSequentialHandlers",
101
+ ".runParallelHandlers",
102
+ )
103
+ );
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Execution frame finders
108
+ //
109
+ // These locate the stack frame that identifies who the actual task action or
110
+ // hook handler is, relative to the boundary frame.
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Finds the task action frame immediately above the resolved-task boundary.
115
+ */
116
+ export function getTaskExecutionFrame(
117
+ frames: StackFrame[],
118
+ ): StackFrame | undefined {
119
+ const resolvedTaskRunIndex = frames.findIndex(isTaskActionBoundaryFrame);
120
+
121
+ if (resolvedTaskRunIndex === -1) {
122
+ return;
123
+ }
124
+
125
+ // The frames between `task.run` calls may include other resolved-task.ts
126
+ // helpers; we want the last frame that does not belong to that file.
127
+ return frames
128
+ .slice(0, resolvedTaskRunIndex)
129
+ .findLast(
130
+ (frame) =>
131
+ frame.location.includes("/internal/core/tasks/resolved-task.") ===
132
+ false,
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Finds the hook handler frame immediately above the hook-manager boundary.
138
+ */
139
+ export function getHookExecutionFrame(
140
+ frames: StackFrame[],
141
+ ): StackFrame | undefined {
142
+ const hookManagerIndex = frames.findIndex(isHookHandlerBoundaryFrame);
143
+
144
+ if (hookManagerIndex === -1) {
145
+ return;
146
+ }
147
+
148
+ // The frames between hook-manager calls may include other hook-manager
149
+ // helpers; we want the last frame that does not belong to that file.
150
+ return frames
151
+ .slice(0, hookManagerIndex)
152
+ .findLast(
153
+ (frame) =>
154
+ frame.location.includes("/internal/core/hook-manager.") === false,
155
+ );
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Frame origin / ownership helpers
160
+ // ---------------------------------------------------------------------------
161
+
162
+ /**
163
+ * Returns true for stack locations owned by packages outside Hardhat.
164
+ */
165
+ export function isThirdPartyFrame(location: string): boolean {
166
+ return (
167
+ location.includes("/node_modules/") &&
168
+ isFirstPartyPluginFrame(location) === false
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Returns true for stack locations owned by Hardhat or first-party packages.
174
+ */
175
+ export function isFirstPartyPluginFrame(location: string): boolean {
176
+ return includesAny(
177
+ location,
178
+ "/node_modules/hardhat/",
179
+ "/node_modules/@nomicfoundation/",
180
+ );
181
+ }
182
+
183
+ /**
184
+ * Returns true when the error stack appears to come from workspace
185
+ * initialization.
186
+ */
187
+ export function isWorkspaceInitFilesystemFrame(error: Error): boolean {
188
+ return error.stack?.includes("/internal/cli/init") ?? false;
189
+ }
190
+
191
+ /**
192
+ * Returns true for stack frames owned by EDR provider or stack-trace code.
193
+ */
194
+ export function isEdrFrame(frame: StackFrame): boolean {
195
+ return includesAny(
196
+ frame.location,
197
+ "/builtin-plugins/network-manager/edr/",
198
+ "/edr-provider.",
199
+ );
200
+ }
@@ -0,0 +1,140 @@
1
+ import { HardhatError } from "@nomicfoundation/hardhat-errors";
2
+
3
+ import {
4
+ type UserCodeBoundaryCategory,
5
+ ErrorCategory,
6
+ USER_CODE_BOUNDARY_FRAME_MATCHERS,
7
+ } from "./classifier.js";
8
+ import { FrameOrigin, createErrorContext } from "./helpers.js";
9
+
10
+ /**
11
+ * Decides if an error should be reported to Sentry or not, based on the
12
+ * category returned by `classifyError(error)`.
13
+ *
14
+ * This first version intentionally uses a permissive policy: it drops clear
15
+ * noise, reports categories that are likely to be Hardhat bugs, and only uses a
16
+ * simple stack-shape heuristic for errors coming from user/plugin execution.
17
+ *
18
+ * @param error The error.
19
+ * @param category The result of calling `classifyError(error)`.
20
+ * @returns `true` if the error should be reported.
21
+ */
22
+ export function shouldBeReported(
23
+ error: Error,
24
+ category: ErrorCategory,
25
+ ): boolean {
26
+ switch (category) {
27
+ case ErrorCategory.CJS_TO_ESM_MIGRATION_ERROR:
28
+ case ErrorCategory.HH2_TO_HH3_MIGRATION_ERROR:
29
+ case ErrorCategory.TYPESCRIPT_SUPPORT_ERROR:
30
+ case ErrorCategory.DEVELOPMENT_TIME_ERROR:
31
+ case ErrorCategory.PROVIDER_INTERACTION_ERROR:
32
+ case ErrorCategory.NETWORK_INTERACTION_ERROR:
33
+ case ErrorCategory.RUNTIME_ENVIRONMENT_ERROR:
34
+ return false;
35
+ case ErrorCategory.HARDHAT_ERROR:
36
+ case ErrorCategory.TASK_ACTION_ERROR:
37
+ case ErrorCategory.EDR_ERROR:
38
+ case ErrorCategory.FILESYSTEM_INTERACTION_ERROR:
39
+ case ErrorCategory.UNEXPECTED_ERROR:
40
+ return shouldReportNonUserCodeBoundaryError(error);
41
+ case ErrorCategory.CONFIG_LOADING_ERROR:
42
+ case ErrorCategory.CONSOLE_EVALUATION_ERROR:
43
+ case ErrorCategory.SCRIPT_EXECUTION_ERROR:
44
+ case ErrorCategory.NODE_TEST_EXECUTION_ERROR:
45
+ case ErrorCategory.MOCHA_TEST_EXECUTION_ERROR:
46
+ case ErrorCategory.PLUGIN_TASK_ACTION_ERROR:
47
+ case ErrorCategory.USER_TASK_ACTION_ERROR:
48
+ case ErrorCategory.PLUGIN_HOOK_HANDLER_ERROR:
49
+ return shouldReportUserCodeBoundaryError(error, category);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Applies the HardhatError descriptor reporting policy for categories that
55
+ * don't need user-code boundary inspection.
56
+ */
57
+ function shouldReportNonUserCodeBoundaryError(error: Error): boolean {
58
+ if (HardhatError.isHardhatError(error)) {
59
+ return error.descriptor.shouldBeReported ?? false;
60
+ }
61
+
62
+ return true;
63
+ }
64
+
65
+ /**
66
+ * Applies the HardhatError descriptor policy before using stack shape to decide
67
+ * whether a user-code-boundary category should be reported.
68
+ */
69
+ function shouldReportUserCodeBoundaryError(
70
+ error: Error,
71
+ category: UserCodeBoundaryCategory,
72
+ ): boolean {
73
+ if (HardhatError.isHardhatError(error)) {
74
+ return error.descriptor.shouldBeReported ?? false;
75
+ }
76
+
77
+ return hasHardhatFrameBeforeBoundary(error, category);
78
+ }
79
+
80
+ /**
81
+ * User-code-boundary categories mean Hardhat called into user or plugin code.
82
+ * We don't want to report every plain user script/test/config/plugin failure,
83
+ * but we do want to report when the stack shows user/plugin code calling back
84
+ * into Hardhat and then Hardhat, or one of its dependencies, failing.
85
+ *
86
+ * To approximate that, this looks for a stack segment before the category's
87
+ * boundary frame with this shape, from the throw site down:
88
+ *
89
+ * dependency frames, optional
90
+ * Hardhat frame, at least one
91
+ * external frames, optional
92
+ * boundary frame
93
+ *
94
+ * In stack-array order, that means scanning from the throw site down to the
95
+ * boundary and looking for a first-party Hardhat frame. If a user project frame
96
+ * appears before that Hardhat frame, we treat the failure as user-owned and
97
+ * don't report it.
98
+ *
99
+ * If no boundary frame is found in the error chain, we report the error. The
100
+ * classifier has already assigned one of these boundary categories, so a
101
+ * missing boundary frame means the filter couldn't validate the expected stack
102
+ * shape. For this initial permissive filter, that should fail open to avoid
103
+ * underreporting due to async stacks, wrapping, or parser limitations.
104
+ *
105
+ * We may reconsider making the external frames required in the future, as this
106
+ * may be too much noise.
107
+ */
108
+ function hasHardhatFrameBeforeBoundary(
109
+ error: Error,
110
+ category: UserCodeBoundaryCategory,
111
+ ): boolean {
112
+ const context = createErrorContext(error);
113
+ const boundaryMatcher = USER_CODE_BOUNDARY_FRAME_MATCHERS[category];
114
+ let boundaryFrameFound = false;
115
+
116
+ for (const candidate of context.errorChain) {
117
+ const frames = context.stackFramesByError.get(candidate) ?? [];
118
+ const boundaryIndex = frames.findIndex(boundaryMatcher);
119
+
120
+ if (boundaryIndex === -1) {
121
+ continue;
122
+ }
123
+
124
+ boundaryFrameFound = true;
125
+
126
+ for (let i = 0; i < boundaryIndex; i++) {
127
+ const frame = frames[i];
128
+
129
+ if (frame.origin === FrameOrigin.USER_PROJECT) {
130
+ return false;
131
+ }
132
+
133
+ if (frame.origin === FrameOrigin.FIRST_PARTY) {
134
+ return true;
135
+ }
136
+ }
137
+ }
138
+
139
+ return !boundaryFrameFound;
140
+ }
@@ -0,0 +1,235 @@
1
+ export enum FrameOrigin {
2
+ FIRST_PARTY = "FIRST_PARTY",
3
+ THIRD_PARTY = "THIRD_PARTY",
4
+ USER_PROJECT = "USER_PROJECT",
5
+ NODE_INTERNAL = "NODE_INTERNAL",
6
+ OTHER = "OTHER",
7
+ }
8
+
9
+ export interface StackFrame {
10
+ functionName?: string;
11
+ location: string;
12
+ origin: FrameOrigin;
13
+ }
14
+
15
+ /**
16
+ * The error context encapsulates the shared derived data used by classification
17
+ * and filtering.
18
+ */
19
+ export interface ErrorContext {
20
+ error: Error;
21
+ errorChain: Error[];
22
+ lowercaseMessageByError: Map<Error, string>;
23
+ stackFramesByError: Map<Error, StackFrame[]>;
24
+ allStackFrames: StackFrame[];
25
+ }
26
+
27
+ /**
28
+ * Builds the shared derived data used by classification and filtering.
29
+ *
30
+ * This keeps stack parsing and cause-chain traversal consistent across
31
+ * matchers, and avoids recomputing them for every category heuristic.
32
+ */
33
+ export function createErrorContext(error: Error): ErrorContext {
34
+ const errorChain = getErrorChain(error);
35
+ const stackFramesByError = new Map(
36
+ errorChain.map((candidate) => [candidate, parseStackFrames(candidate)]),
37
+ );
38
+
39
+ return {
40
+ error,
41
+ errorChain,
42
+ lowercaseMessageByError: new Map(
43
+ errorChain.map((candidate) => [
44
+ candidate,
45
+ candidate.message.toLowerCase(),
46
+ ]),
47
+ ),
48
+ stackFramesByError,
49
+ allStackFrames: errorChain.flatMap(
50
+ (candidate) => stackFramesByError.get(candidate) ?? [],
51
+ ),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * This function should be used instead of instanceof because it is robust
57
+ * under the presence of multiple installations of the same package (e.g.
58
+ * multiple hardhat-utils versions).
59
+ *
60
+ * @param error The error
61
+ * @param errorClass The error class
62
+ * @returns true if the error has the same name as the error class
63
+ */
64
+ export function hasErrorClassName(
65
+ error: Error,
66
+ errorClass: abstract new (...args: never[]) => Error,
67
+ ): boolean {
68
+ return error.name === errorClass.name;
69
+ }
70
+
71
+ /**
72
+ * Returns true when `value` contains any of the supplied substrings.
73
+ */
74
+ export function includesAny(
75
+ value: string | undefined,
76
+ ...substrings: string[]
77
+ ): boolean {
78
+ return (
79
+ value !== undefined &&
80
+ substrings.some((substring) => value.includes(substring))
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Returns a Node-style `code` string from an error or any Error cause.
86
+ *
87
+ * Traversal stops when a cause is not an Error, a cycle is detected, or
88
+ * `maxCauseDepth` is reached.
89
+ */
90
+ export function getNodeErrorCode(
91
+ error: Error,
92
+ maxCauseDepth = 10,
93
+ ): string | undefined {
94
+ const seen = new Set<Error>();
95
+ let current: Error | undefined = error;
96
+ let depth = 0;
97
+
98
+ while (current !== undefined && depth < maxCauseDepth && !seen.has(current)) {
99
+ if ("code" in current && typeof current.code === "string") {
100
+ return current.code;
101
+ }
102
+
103
+ seen.add(current);
104
+ current = getCause(current);
105
+ depth++;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Returns the error and its nested causes in outer-to-inner order.
111
+ *
112
+ * Traversal stops when a cause is not an Error, a cycle is detected, or
113
+ * `maxCauseDepth` is reached.
114
+ */
115
+ function getErrorChain(error: Error, maxCauseDepth = 10): Error[] {
116
+ const errors: Error[] = [];
117
+ const seen = new Set<Error>();
118
+
119
+ let current: Error | undefined = error;
120
+ while (
121
+ current !== undefined &&
122
+ errors.length < maxCauseDepth &&
123
+ seen.has(current) === false
124
+ ) {
125
+ errors.push(current);
126
+ seen.add(current);
127
+
128
+ if (current.cause !== undefined && !(current.cause instanceof Error)) {
129
+ break;
130
+ }
131
+
132
+ current = getCause(current);
133
+ }
134
+
135
+ return errors;
136
+ }
137
+
138
+ /**
139
+ * Parses V8-style stack lines into normalized stack frames.
140
+ *
141
+ * Unrecognized lines are ignored, and path separators are normalized to `/`
142
+ * before the frame origin is inferred.
143
+ */
144
+ function parseStackFrames(error: Error): StackFrame[] {
145
+ if (error.stack === undefined) {
146
+ return [];
147
+ }
148
+
149
+ return error.stack
150
+ .split("\n")
151
+ .slice(1)
152
+ .map((line) => line.trim())
153
+ .map(parseStackFrameLine)
154
+ .filter((frame): frame is StackFrame => frame !== undefined);
155
+ }
156
+
157
+ /**
158
+ * Parses a single V8 stack frame line.
159
+ */
160
+ function parseStackFrameLine(line: string): StackFrame | undefined {
161
+ const match =
162
+ line.match(/^at (?:(.+?) \()?(.+?):\d+:\d+\)?$/) ??
163
+ line.match(/^at (?:(.+?) \()?(.+?)\)?$/);
164
+
165
+ if (match === null || match[2] === undefined) {
166
+ return;
167
+ }
168
+
169
+ const functionName = match[1] === undefined ? undefined : match[1].trim();
170
+ const location = normalizeLocation(match[2]);
171
+
172
+ return {
173
+ functionName,
174
+ location,
175
+ origin: getFrameOrigin(location),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Returns the Error-valued cause of an error, ignoring non-Error causes.
181
+ */
182
+ function getCause(error: Error): Error | undefined {
183
+ if ("cause" in error && error.cause instanceof Error) {
184
+ return error.cause;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Normalizes Windows paths and file URLs enough for substring-based matchers.
190
+ */
191
+ function normalizeLocation(location: string): string {
192
+ return location.replaceAll("\\", "/");
193
+ }
194
+
195
+ /**
196
+ * Infers who owns a stack frame from its normalized location.
197
+ */
198
+ function getFrameOrigin(location: string): FrameOrigin {
199
+ if (
200
+ startsWithAny(location, "node:", "internal/") ||
201
+ includesAny(location, "node:internal/")
202
+ ) {
203
+ return FrameOrigin.NODE_INTERNAL;
204
+ }
205
+
206
+ if (location.includes("/node_modules/")) {
207
+ if (
208
+ includesAny(
209
+ location,
210
+ "/node_modules/hardhat/",
211
+ "/node_modules/@nomicfoundation/",
212
+ )
213
+ ) {
214
+ return FrameOrigin.FIRST_PARTY;
215
+ }
216
+
217
+ return FrameOrigin.THIRD_PARTY;
218
+ }
219
+
220
+ if (
221
+ startsWithAny(location, "/", "file://", "[eval]") ||
222
+ /^[A-Za-z]:\//.test(location)
223
+ ) {
224
+ return FrameOrigin.USER_PROJECT;
225
+ }
226
+
227
+ return FrameOrigin.OTHER;
228
+ }
229
+
230
+ /**
231
+ * Returns true when `value` starts with any of the supplied prefixes.
232
+ */
233
+ function startsWithAny(value: string, ...prefixes: string[]): boolean {
234
+ return prefixes.some((prefix) => value.startsWith(prefix));
235
+ }
@@ -1,9 +1,16 @@
1
+ import type * as ClassifierT from "../error-classification/classifier.js";
2
+ import type * as FilterT from "../error-classification/filter.js";
1
3
  import type * as SentryReporterT from "../sentry/reporter.js";
2
4
 
3
5
  // Sentry's reporter loads a large number of modules, so we only load it if
4
6
  // needed.
5
7
  let sentryReporterModule: typeof SentryReporterT | undefined;
6
8
 
9
+ // The classifier and filter modules are small, but they may import many
10
+ // unrelated things top-level to do their job, so we also load them lazily.
11
+ let classifierModule: typeof ClassifierT | undefined;
12
+ let filterModule: typeof FilterT | undefined;
13
+
7
14
  // We cache the `setCliHardhatConfigPath` to avoid loading the reporter just
8
15
  // for this setting. We load it and set the config path if needed.
9
16
  let cliHardhatConfigPath: string | undefined;
@@ -29,6 +36,20 @@ export async function sendErrorTelemetry(
29
36
  error: Error,
30
37
  hint?: { unhandled?: boolean; mechanismType?: string },
31
38
  ): Promise<void> {
39
+ if (classifierModule === undefined) {
40
+ classifierModule = await import("../error-classification/classifier.js");
41
+ }
42
+
43
+ if (filterModule === undefined) {
44
+ filterModule = await import("../error-classification/filter.js");
45
+ }
46
+
47
+ const category = classifierModule.classifyError(error);
48
+
49
+ if (!filterModule.shouldBeReported(error, category)) {
50
+ return;
51
+ }
52
+
32
53
  if (sentryReporterModule === undefined) {
33
54
  sentryReporterModule = await import("../sentry/reporter.js");
34
55
  }