UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

205 lines (174 loc) 5.42 kB
import type { Envelope, Transport, TransportMakeRequestResponse, } from "@sentry/core"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createTransport, serializeEnvelope } from "@sentry/core"; import debug from "debug"; const log = debug("hardhat:core:sentry:transport"); /** * Creates a detached process transport. * * This transport spawns a detached process synchronously and sends the envelope * from there. * * This means that the Hardhat process doesn't have to wait for the request * to finish before exiting, flushing the transport, not closing the client. * * This is meant to be use as THE main transport in Hardhat. * * @param dsn The DSN to use to send the envelope to Sentry. * @param release The release/version of Hardhat. * @param environment The environment of Hardhat. * @param configPath The path to the config file. */ export function createDetachedProcessTransport( dsn: string, release: string, environment: string, getConfigPath: () => string | undefined, ): Transport { return { send: (envelope) => { const verbose = log.enabled; // **Synchronously** spawn a detached subprocess here const out = verbose ? process.stdout : "ignore"; const err = verbose ? process.stdout : "ignore"; const subprocessPath = import.meta.url.endsWith(".ts") ? fileURLToPath(import.meta.resolve("./subprocess.ts")) : fileURLToPath(import.meta.resolve("./subprocess.js")); const serializedEnvelope = JSON.stringify(envelope); let args = [ subprocessPath, serializedEnvelope, getConfigPath() ?? "", dsn, release, environment, ]; if (isTsxRequiredForSubprocess(subprocessPath)) { args = ["--import", "tsx/esm", ...args]; } log(`Spawning reporter subprocess`); const env = { ...process.env }; const subprocess = spawn(process.execPath, args, { detached: true, stdio: ["ignore", out, err], shell: false, env, }); subprocess.unref(); return Promise.resolve({ statusCode: 200 }); }, flush: (_timeout) => { return Promise.resolve(true); }, }; } /** * This is a `fetch`-backed transport that sends the envelope to Sentry's * backend. * * This is meant to be the fallback transport that is used in the detached * process that backs the other transport. * * If you use this transport, you should call `close` on the client before * exiting the process. */ export function createHttpTransport(dsn: string): Transport { return createTransport( { recordDroppedEvent(reason, category, count) { log( `Dropped event: ${reason} (category: ${category} - count: ${count})`, ); }, }, async (request) => { try { log(`Sending envelope to Sentry backend using the HttpTransport`); const response = await sendSerializedEnvelopeToSentryBackend( dsn, request.body, ); log( `Successfully sent envelope to Sentry backend using the HttpTransport`, ); return response; } catch (e) { log( `Failed to send envelope to Sentry backend using the HttpTransport`, e, ); throw e; } }, ); } /** * Sends an envelope to Sentry's backend. * * This function is used both in the subprocess (to send envelopes received * from the main process) and as the core implementation for the * `createHttpTransport` transport. */ export async function sendEnvelopeToSentryBackend( dsn: string, envelope: Envelope, ): Promise<TransportMakeRequestResponse> { return sendSerializedEnvelopeToSentryBackend( dsn, serializeEnvelope(envelope), ); } function isTsxRequiredForSubprocess(subprocessPath: string): boolean { const tsNativeRuntimes = ["Deno", "Bun"]; if (tsNativeRuntimes.some((env) => env in globalThis)) { return false; } return subprocessPath.endsWith(".ts"); } async function sendSerializedEnvelopeToSentryBackend( dsn: string, serializedEnvelope: string | Uint8Array, ): Promise<TransportMakeRequestResponse> { const { hostname, username: publicKey, password: secret, pathname, } = new URL(dsn); const projectId = pathname.replace(/^\//, ""); const ingestUrl = `https://${hostname}/api/${projectId}/envelope/`; const authHeader = [ "Sentry sentry_version=7", `sentry_client=hardhat/3.0.0`, `sentry_key=${publicKey}`, secret !== "" && `sentry_secret=${secret}`, ] .filter(Boolean) .join(", "); const res = await fetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/x-sentry-envelope", "X-Sentry-Auth": authHeader, }, body: serializedEnvelope, }); if (!res.ok) { const text = await res.text(); /* eslint-disable-next-line no-restricted-syntax -- Only run in the subprocess, so we don't care about the error type */ throw new Error(`Failed to send envelope: ${res.status} - ${text}`); } return { statusCode: res.status, /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We return the headers as any, because we just want to return whatever we received from the server. */ headers: res.headers as any, }; }