Skip to content

Commit

Permalink
feat: Add finer Sentry HTTP Tracing (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholas-codecov authored Jan 13, 2025
1 parent 88c2eb1 commit 07ce9fb
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 32 deletions.
10 changes: 7 additions & 3 deletions packages/bundler-plugin-core/src/utils/Output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class Output {
scope: this.sentryScope,
parentSpan: outputWriteSpan,
},
async () => {
async (getPreSignedURLSpan) => {
let url = "";
try {
url = await getPreSignedURL({
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
99 changes: 87 additions & 12 deletions packages/bundler-plugin-core/src/utils/getPreSignedURL.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -21,6 +29,8 @@ interface GetPreSignedURLArgs {
useGitHubOIDC: boolean;
gitHubOIDCTokenAudience: string;
};
sentryScope?: Scope;
sentrySpan?: Span;
}

type RequestBody = Record<string, string | null | undefined>;
Expand All @@ -38,6 +48,8 @@ export const getPreSignedURL = async ({
retryCount,
gitService,
oidc,
sentryScope,
sentrySpan,
}: GetPreSignedURLArgs) => {
const headers = new Headers({
"Content-Type": "application/json",
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion packages/bundler-plugin-core/src/utils/normalizeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof optionsSchemaFactory>
> &
Expand Down Expand Up @@ -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.",
Expand Down
79 changes: 63 additions & 16 deletions packages/bundler-plugin-core/src/utils/uploadStats.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,13 +12,17 @@ interface UploadStatsArgs {
bundleName: string;
preSignedUrl: string;
retryCount?: number;
sentryScope?: Scope;
sentrySpan?: Span;
}

export async function uploadStats({
message,
bundleName,
preSignedUrl,
retryCount,
sentryScope,
sentrySpan,
}: UploadStatsArgs) {
const iterator = message[Symbol.iterator]();
const stream = new ReadableStream({
Expand All @@ -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) {
Expand Down

0 comments on commit 07ce9fb

Please sign in to comment.