Skip to content

Commit 07ce9fb

Browse files
authored
feat: Add finer Sentry HTTP Tracing (#237)
1 parent 88c2eb1 commit 07ce9fb

File tree

4 files changed

+160
-32
lines changed

4 files changed

+160
-32
lines changed

packages/bundler-plugin-core/src/utils/Output.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ class Output {
239239
scope: this.sentryScope,
240240
parentSpan: outputWriteSpan,
241241
},
242-
async () => {
242+
async (getPreSignedURLSpan) => {
243243
let url = "";
244244
try {
245245
url = await getPreSignedURL({
@@ -249,6 +249,8 @@ class Output {
249249
oidc: this.oidc,
250250
retryCount: this.retryCount,
251251
serviceParams: provider,
252+
sentryScope: this.sentryScope,
253+
sentrySpan: getPreSignedURLSpan,
252254
});
253255
} catch (error) {
254256
if (this.sentryClient && this.sentryScope) {
@@ -291,13 +293,15 @@ class Output {
291293
scope: this.sentryScope,
292294
parentSpan: outputWriteSpan,
293295
},
294-
async () => {
296+
async (uploadStatsSpan) => {
295297
try {
296298
await uploadStats({
297299
preSignedUrl: presignedURL,
298300
bundleName: this.bundleName,
299301
message: this.bundleStatsToJson(),
300-
retryCount: this?.retryCount,
302+
retryCount: this.retryCount,
303+
sentryScope: this.sentryScope,
304+
sentrySpan: uploadStatsSpan,
301305
});
302306
} catch (error) {
303307
// this is being set as an error because this could not be caused by a user error

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

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import * as Core from "@actions/core";
2+
import {
3+
spanToTraceHeader,
4+
spanToBaggageHeader,
5+
startSpan,
6+
type Scope,
7+
type Span,
8+
} from "@sentry/core";
29
import { z } from "zod";
310
import { FailedFetchError } from "../errors/FailedFetchError.ts";
411
import { UploadLimitReachedError } from "../errors/UploadLimitReachedError.ts";
@@ -10,6 +17,7 @@ import { findGitService } from "./findGitService.ts";
1017
import { UndefinedGitServiceError } from "../errors/UndefinedGitServiceError.ts";
1118
import { FailedOIDCFetchError } from "../errors/FailedOIDCFetchError.ts";
1219
import { BadOIDCServiceError } from "../errors/BadOIDCServiceError.ts";
20+
import { DEFAULT_API_URL } from "./normalizeOptions.ts";
1321

1422
interface GetPreSignedURLArgs {
1523
apiUrl: string;
@@ -21,6 +29,8 @@ interface GetPreSignedURLArgs {
2129
useGitHubOIDC: boolean;
2230
gitHubOIDCTokenAudience: string;
2331
};
32+
sentryScope?: Scope;
33+
sentrySpan?: Span;
2434
}
2535

2636
type RequestBody = Record<string, string | null | undefined>;
@@ -38,6 +48,8 @@ export const getPreSignedURL = async ({
3848
retryCount,
3949
gitService,
4050
oidc,
51+
sentryScope,
52+
sentrySpan,
4153
}: GetPreSignedURLArgs) => {
4254
const headers = new Headers({
4355
"Content-Type": "application/json",
@@ -84,22 +96,85 @@ export const getPreSignedURL = async ({
8496
}
8597
}
8698

99+
// Add Sentry headers if the API URL is the default i.e. Codecov itself
100+
if (sentrySpan && apiUrl === DEFAULT_API_URL) {
101+
// Create `sentry-trace` header
102+
const sentryTraceHeader = spanToTraceHeader(sentrySpan);
103+
104+
// Create `baggage` header
105+
const sentryBaggageHeader = spanToBaggageHeader(sentrySpan);
106+
107+
if (sentryTraceHeader && sentryBaggageHeader) {
108+
headers.set("sentry-trace", sentryTraceHeader);
109+
headers.set("baggage", sentryBaggageHeader);
110+
}
111+
}
112+
87113
let response: Response;
88114
try {
89-
const body = preProcessBody(requestBody);
90-
response = await fetchWithRetry({
91-
retryCount,
92-
url: `${apiUrl}${API_ENDPOINT}`,
93-
name: "`get-pre-signed-url`",
94-
requestData: {
95-
method: "POST",
96-
headers: headers,
97-
body: JSON.stringify(body),
115+
response = await startSpan(
116+
{
117+
name: "Fetching Pre-Signed URL",
118+
op: "http.client",
119+
scope: sentryScope,
120+
parentSpan: sentrySpan,
98121
},
99-
});
122+
async (getPreSignedURLSpan) => {
123+
let wrappedResponse: Response;
124+
const HTTP_METHOD = "POST";
125+
const URL = `${apiUrl}${API_ENDPOINT}`;
126+
127+
if (getPreSignedURLSpan) {
128+
getPreSignedURLSpan.setAttribute("http.request.method", HTTP_METHOD);
129+
}
130+
131+
// we only want to set the URL attribute if the API URL is the default i.e. Codecov itself
132+
if (getPreSignedURLSpan && apiUrl === DEFAULT_API_URL) {
133+
getPreSignedURLSpan.setAttribute("http.request.url", URL);
134+
}
135+
136+
try {
137+
const body = preProcessBody(requestBody);
138+
wrappedResponse = await fetchWithRetry({
139+
retryCount,
140+
url: URL,
141+
name: "`get-pre-signed-url`",
142+
requestData: {
143+
method: HTTP_METHOD,
144+
headers: headers,
145+
body: JSON.stringify(body),
146+
},
147+
});
148+
} catch (e) {
149+
red("Failed to fetch pre-signed URL");
150+
throw new FailedFetchError("Failed to fetch pre-signed URL", {
151+
cause: e,
152+
});
153+
}
154+
155+
// Add attributes only if the span is present
156+
if (getPreSignedURLSpan) {
157+
// Set attributes for the response
158+
getPreSignedURLSpan.setAttribute(
159+
"http.response.status_code",
160+
wrappedResponse.status,
161+
);
162+
getPreSignedURLSpan.setAttribute(
163+
"http.response_content_length",
164+
Number(wrappedResponse.headers.get("content-length")),
165+
);
166+
getPreSignedURLSpan.setAttribute(
167+
"http.response.status_text",
168+
wrappedResponse.statusText,
169+
);
170+
}
171+
172+
return wrappedResponse;
173+
},
174+
);
100175
} catch (e) {
101-
red("Failed to fetch pre-signed URL");
102-
throw new FailedFetchError("Failed to fetch pre-signed URL", { cause: e });
176+
// re-throwing the error here
177+
throw e;
103178
}
104179

105180
if (response.status === 429) {

packages/bundler-plugin-core/src/utils/normalizeOptions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { z } from "zod";
22
import { type Options } from "../types.ts";
33
import { red } from "./logging.ts";
44

5+
export const DEFAULT_API_URL = "https://api.codecov.io";
6+
57
export type NormalizedOptions = z.infer<
68
ReturnType<typeof optionsSchemaFactory>
79
> &
@@ -87,7 +89,7 @@ const optionsSchemaFactory = (options: Options) =>
8789
.url({
8890
message: `apiUrl: \`${options?.apiUrl}\` is not a valid URL.`,
8991
})
90-
.default("https://api.codecov.io"),
92+
.default(DEFAULT_API_URL),
9193
bundleName: z
9294
.string({
9395
invalid_type_error: "`bundleName` must be a string.",

packages/bundler-plugin-core/src/utils/uploadStats.ts

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReadableStream, TextEncoderStream } from "node:stream/web";
2+
import { startSpan, type Scope, type Span } from "@sentry/core";
23

34
import { FailedUploadError } from "../errors/FailedUploadError";
45
import { green, red } from "./logging";
@@ -11,13 +12,17 @@ interface UploadStatsArgs {
1112
bundleName: string;
1213
preSignedUrl: string;
1314
retryCount?: number;
15+
sentryScope?: Scope;
16+
sentrySpan?: Span;
1417
}
1518

1619
export async function uploadStats({
1720
message,
1821
bundleName,
1922
preSignedUrl,
2023
retryCount,
24+
sentryScope,
25+
sentrySpan,
2126
}: UploadStatsArgs) {
2227
const iterator = message[Symbol.iterator]();
2328
const stream = new ReadableStream({
@@ -33,25 +38,67 @@ export async function uploadStats({
3338
}).pipeThrough(new TextEncoderStream());
3439

3540
let response: Response;
41+
3642
try {
37-
response = await fetchWithRetry({
38-
url: preSignedUrl,
39-
retryCount,
40-
name: "`upload-stats`",
41-
requestData: {
42-
method: "PUT",
43-
headers: {
44-
"Content-Type": "application/json",
45-
},
46-
duplex: "half",
47-
// @ts-expect-error TypeScript doesn't know that fetch can accept a
48-
// ReadableStream as the body
49-
body: stream,
43+
response = await startSpan(
44+
{
45+
name: "Uploading Stats",
46+
op: "http.client",
47+
scope: sentryScope,
48+
parentSpan: sentrySpan,
5049
},
51-
});
50+
async (uploadStatsSpan) => {
51+
let wrappedResponse: Response;
52+
const HTTP_METHOD = "PUT";
53+
54+
if (uploadStatsSpan) {
55+
// we're not collecting the URL here because its a pre-signed URL
56+
uploadStatsSpan.setAttribute("http.request.method", HTTP_METHOD);
57+
}
58+
59+
try {
60+
wrappedResponse = await fetchWithRetry({
61+
url: preSignedUrl,
62+
retryCount,
63+
name: "`upload-stats`",
64+
requestData: {
65+
method: HTTP_METHOD,
66+
headers: {
67+
"Content-Type": "application/json",
68+
},
69+
duplex: "half",
70+
// @ts-expect-error TypeScript doesn't know that fetch can accept a
71+
// ReadableStream as the body
72+
body: stream,
73+
},
74+
});
75+
} catch (e) {
76+
red("Failed to upload stats, fetch failed");
77+
throw new FailedFetchError("Failed to upload stats");
78+
}
79+
80+
if (uploadStatsSpan) {
81+
// Set attributes for the response
82+
uploadStatsSpan.setAttribute(
83+
"http.response.status_code",
84+
wrappedResponse.status,
85+
);
86+
uploadStatsSpan.setAttribute(
87+
"http.response_content_length",
88+
Number(wrappedResponse.headers.get("content-length")),
89+
);
90+
uploadStatsSpan.setAttribute(
91+
"http.response.status_text",
92+
wrappedResponse.statusText,
93+
);
94+
}
95+
96+
return wrappedResponse;
97+
},
98+
);
5299
} catch (e) {
53-
red("Failed to upload stats, fetch failed");
54-
throw new FailedFetchError("Failed to upload stats");
100+
// just re-throwing the error here
101+
throw e;
55102
}
56103

57104
if (response.status === 429) {

0 commit comments

Comments
 (0)