From 6b71b012d2407f2a16a1421903cc87d17132c1b5 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Fri, 10 Jan 2025 08:01:04 -0400 Subject: [PATCH 1/3] move the default API value to a const --- packages/bundler-plugin-core/src/utils/normalizeOptions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bundler-plugin-core/src/utils/normalizeOptions.ts b/packages/bundler-plugin-core/src/utils/normalizeOptions.ts index 6efff8b2..dd6a29cb 100644 --- a/packages/bundler-plugin-core/src/utils/normalizeOptions.ts +++ b/packages/bundler-plugin-core/src/utils/normalizeOptions.ts @@ -2,6 +2,8 @@ import { z } from "zod"; import { type Options } from "../types.ts"; import { red } from "./logging.ts"; +export const DEFAULT_API_URL = "https://api.codecov.io"; + export type NormalizedOptions = z.infer< ReturnType > & @@ -87,7 +89,7 @@ const optionsSchemaFactory = (options: Options) => .url({ message: `apiUrl: \`${options?.apiUrl}\` is not a valid URL.`, }) - .default("https://api.codecov.io"), + .default(DEFAULT_API_URL), bundleName: z .string({ invalid_type_error: "`bundleName` must be a string.", From 72b7a2c9e2caad4ada4256037c23382f573bd6e0 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Fri, 10 Jan 2025 08:06:14 -0400 Subject: [PATCH 2/3] pass down sentry things to getPresignedURL and uploadStats --- packages/bundler-plugin-core/src/utils/Output.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/bundler-plugin-core/src/utils/Output.ts b/packages/bundler-plugin-core/src/utils/Output.ts index 033bbaa0..7b6d8563 100644 --- a/packages/bundler-plugin-core/src/utils/Output.ts +++ b/packages/bundler-plugin-core/src/utils/Output.ts @@ -239,7 +239,7 @@ class Output { scope: this.sentryScope, parentSpan: outputWriteSpan, }, - async () => { + async (getPreSignedURLSpan) => { let url = ""; try { url = await getPreSignedURL({ @@ -249,6 +249,8 @@ class Output { oidc: this.oidc, retryCount: this.retryCount, serviceParams: provider, + sentryScope: this.sentryScope, + sentrySpan: getPreSignedURLSpan, }); } catch (error) { if (this.sentryClient && this.sentryScope) { @@ -291,13 +293,15 @@ class Output { scope: this.sentryScope, parentSpan: outputWriteSpan, }, - async () => { + async (uploadStatsSpan) => { try { await uploadStats({ preSignedUrl: presignedURL, bundleName: this.bundleName, message: this.bundleStatsToJson(), - retryCount: this?.retryCount, + retryCount: this.retryCount, + sentryScope: this.sentryScope, + sentrySpan: uploadStatsSpan, }); } catch (error) { // this is being set as an error because this could not be caused by a user error From 4b51c5e97000246b9e552ae91081a92c1f678764 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Fri, 10 Jan 2025 08:06:40 -0400 Subject: [PATCH 3/3] update getPreSignedURL and uploadStats to collect spans only around fetching --- .../src/utils/getPreSignedURL.ts | 99 ++++++++++++++++--- .../src/utils/uploadStats.ts | 79 ++++++++++++--- 2 files changed, 150 insertions(+), 28 deletions(-) diff --git a/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts b/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts index d47f7af3..4c74228e 100644 --- a/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts +++ b/packages/bundler-plugin-core/src/utils/getPreSignedURL.ts @@ -1,4 +1,11 @@ import * as Core from "@actions/core"; +import { + spanToTraceHeader, + spanToBaggageHeader, + startSpan, + type Scope, + type Span, +} from "@sentry/core"; import { z } from "zod"; import { FailedFetchError } from "../errors/FailedFetchError.ts"; import { UploadLimitReachedError } from "../errors/UploadLimitReachedError.ts"; @@ -10,6 +17,7 @@ import { findGitService } from "./findGitService.ts"; import { UndefinedGitServiceError } from "../errors/UndefinedGitServiceError.ts"; import { FailedOIDCFetchError } from "../errors/FailedOIDCFetchError.ts"; import { BadOIDCServiceError } from "../errors/BadOIDCServiceError.ts"; +import { DEFAULT_API_URL } from "./normalizeOptions.ts"; interface GetPreSignedURLArgs { apiUrl: string; @@ -21,6 +29,8 @@ interface GetPreSignedURLArgs { useGitHubOIDC: boolean; gitHubOIDCTokenAudience: string; }; + sentryScope?: Scope; + sentrySpan?: Span; } type RequestBody = Record; @@ -38,6 +48,8 @@ export const getPreSignedURL = async ({ retryCount, gitService, oidc, + sentryScope, + sentrySpan, }: GetPreSignedURLArgs) => { const headers = new Headers({ "Content-Type": "application/json", @@ -84,22 +96,85 @@ export const getPreSignedURL = async ({ } } + // Add Sentry headers if the API URL is the default i.e. Codecov itself + if (sentrySpan && apiUrl === DEFAULT_API_URL) { + // Create `sentry-trace` header + const sentryTraceHeader = spanToTraceHeader(sentrySpan); + + // Create `baggage` header + const sentryBaggageHeader = spanToBaggageHeader(sentrySpan); + + if (sentryTraceHeader && sentryBaggageHeader) { + headers.set("sentry-trace", sentryTraceHeader); + headers.set("baggage", sentryBaggageHeader); + } + } + let response: Response; try { - const body = preProcessBody(requestBody); - response = await fetchWithRetry({ - retryCount, - url: `${apiUrl}${API_ENDPOINT}`, - name: "`get-pre-signed-url`", - requestData: { - method: "POST", - headers: headers, - body: JSON.stringify(body), + response = await startSpan( + { + name: "Fetching Pre-Signed URL", + op: "http.client", + scope: sentryScope, + parentSpan: sentrySpan, }, - }); + async (getPreSignedURLSpan) => { + let wrappedResponse: Response; + const HTTP_METHOD = "POST"; + const URL = `${apiUrl}${API_ENDPOINT}`; + + if (getPreSignedURLSpan) { + getPreSignedURLSpan.setAttribute("http.request.method", HTTP_METHOD); + } + + // we only want to set the URL attribute if the API URL is the default i.e. Codecov itself + if (getPreSignedURLSpan && apiUrl === DEFAULT_API_URL) { + getPreSignedURLSpan.setAttribute("http.request.url", URL); + } + + try { + const body = preProcessBody(requestBody); + wrappedResponse = await fetchWithRetry({ + retryCount, + url: URL, + name: "`get-pre-signed-url`", + requestData: { + method: HTTP_METHOD, + headers: headers, + body: JSON.stringify(body), + }, + }); + } catch (e) { + red("Failed to fetch pre-signed URL"); + throw new FailedFetchError("Failed to fetch pre-signed URL", { + cause: e, + }); + } + + // Add attributes only if the span is present + if (getPreSignedURLSpan) { + // Set attributes for the response + getPreSignedURLSpan.setAttribute( + "http.response.status_code", + wrappedResponse.status, + ); + getPreSignedURLSpan.setAttribute( + "http.response_content_length", + Number(wrappedResponse.headers.get("content-length")), + ); + getPreSignedURLSpan.setAttribute( + "http.response.status_text", + wrappedResponse.statusText, + ); + } + + return wrappedResponse; + }, + ); } catch (e) { - red("Failed to fetch pre-signed URL"); - throw new FailedFetchError("Failed to fetch pre-signed URL", { cause: e }); + // re-throwing the error here + throw e; } if (response.status === 429) { diff --git a/packages/bundler-plugin-core/src/utils/uploadStats.ts b/packages/bundler-plugin-core/src/utils/uploadStats.ts index 735c4402..a50360c0 100644 --- a/packages/bundler-plugin-core/src/utils/uploadStats.ts +++ b/packages/bundler-plugin-core/src/utils/uploadStats.ts @@ -1,4 +1,5 @@ import { ReadableStream, TextEncoderStream } from "node:stream/web"; +import { startSpan, type Scope, type Span } from "@sentry/core"; import { FailedUploadError } from "../errors/FailedUploadError"; import { green, red } from "./logging"; @@ -11,6 +12,8 @@ interface UploadStatsArgs { bundleName: string; preSignedUrl: string; retryCount?: number; + sentryScope?: Scope; + sentrySpan?: Span; } export async function uploadStats({ @@ -18,6 +21,8 @@ export async function uploadStats({ bundleName, preSignedUrl, retryCount, + sentryScope, + sentrySpan, }: UploadStatsArgs) { const iterator = message[Symbol.iterator](); const stream = new ReadableStream({ @@ -33,25 +38,67 @@ export async function uploadStats({ }).pipeThrough(new TextEncoderStream()); let response: Response; + try { - response = await fetchWithRetry({ - url: preSignedUrl, - retryCount, - name: "`upload-stats`", - requestData: { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - duplex: "half", - // @ts-expect-error TypeScript doesn't know that fetch can accept a - // ReadableStream as the body - body: stream, + response = await startSpan( + { + name: "Uploading Stats", + op: "http.client", + scope: sentryScope, + parentSpan: sentrySpan, }, - }); + async (uploadStatsSpan) => { + let wrappedResponse: Response; + const HTTP_METHOD = "PUT"; + + if (uploadStatsSpan) { + // we're not collecting the URL here because its a pre-signed URL + uploadStatsSpan.setAttribute("http.request.method", HTTP_METHOD); + } + + try { + wrappedResponse = await fetchWithRetry({ + url: preSignedUrl, + retryCount, + name: "`upload-stats`", + requestData: { + method: HTTP_METHOD, + headers: { + "Content-Type": "application/json", + }, + duplex: "half", + // @ts-expect-error TypeScript doesn't know that fetch can accept a + // ReadableStream as the body + body: stream, + }, + }); + } catch (e) { + red("Failed to upload stats, fetch failed"); + throw new FailedFetchError("Failed to upload stats"); + } + + if (uploadStatsSpan) { + // Set attributes for the response + uploadStatsSpan.setAttribute( + "http.response.status_code", + wrappedResponse.status, + ); + uploadStatsSpan.setAttribute( + "http.response_content_length", + Number(wrappedResponse.headers.get("content-length")), + ); + uploadStatsSpan.setAttribute( + "http.response.status_text", + wrappedResponse.statusText, + ); + } + + return wrappedResponse; + }, + ); } catch (e) { - red("Failed to upload stats, fetch failed"); - throw new FailedFetchError("Failed to upload stats"); + // just re-throwing the error here + throw e; } if (response.status === 429) {