Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add finer Sentry HTTP Tracing #237

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { 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 @@
useGitHubOIDC: boolean;
gitHubOIDCTokenAudience: string;
};
sentryScope?: Scope;
sentrySpan?: Span;
}

type RequestBody = Record<string, string | null | undefined>;
Expand All @@ -38,6 +48,8 @@
retryCount,
gitService,
oidc,
sentryScope,
sentrySpan,
}: GetPreSignedURLArgs) => {
const headers = new Headers({
"Content-Type": "application/json",
Expand Down Expand Up @@ -84,22 +96,85 @@
}
}

// 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);

Check warning on line 102 in packages/bundler-plugin-core/src/utils/getPreSignedURL.ts

View check run for this annotation

Codecov Notifications / codecov/patch

packages/bundler-plugin-core/src/utils/getPreSignedURL.ts#L102

Added line #L102 was not covered by tests

// Create `baggage` header
const sentryBaggageHeader = spanToBaggageHeader(sentrySpan);

Check warning on line 105 in packages/bundler-plugin-core/src/utils/getPreSignedURL.ts

View check run for this annotation

Codecov Notifications / codecov/patch

packages/bundler-plugin-core/src/utils/getPreSignedURL.ts#L105

Added line #L105 was not covered by tests

if (sentryTraceHeader && sentryBaggageHeader) {
headers.set("sentry-trace", sentryTraceHeader);
headers.set("baggage", sentryBaggageHeader);
}

Check warning on line 110 in packages/bundler-plugin-core/src/utils/getPreSignedURL.ts

View check run for this annotation

Codecov Notifications / codecov/patch

packages/bundler-plugin-core/src/utils/getPreSignedURL.ts#L107-L110

Added lines #L107 - L110 were not covered by tests
}

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);
}

Check warning on line 134 in packages/bundler-plugin-core/src/utils/getPreSignedURL.ts

View check run for this annotation

Codecov Notifications / codecov/patch

packages/bundler-plugin-core/src/utils/getPreSignedURL.ts#L133-L134

Added lines #L133 - L134 were not covered by tests

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
Loading