Skip to content

Commit ebac8a1

Browse files
Merge pull request #31 from asingh9-godaddy/DEVX-73-http-trace-headers
Devx 73 http trace headers
1 parent 5eac899 commit ebac8a1

10 files changed

Lines changed: 72 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@godaddy/cli": minor
3+
---
4+
5+
Outgoing HTTP requests now carry a consistent `User-Agent` (`godaddy-cli/<version>` from the package manifest) and a per-request `X-Request-ID` (UUID v7) on REST catalog calls, GraphQL requests, OAuth token exchange, and webhook event-type discovery. Presigned S3 artifact uploads are unchanged: only the headers returned with the presigned URL are sent, so uploads remain signature-compatible.

src/core/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "../effect/errors";
1111
import { fileExists } from "../effect/fs-utils";
1212
import type { Keychain } from "../effect/services/keychain";
13+
import { CLI_USER_AGENT } from "../shared/cli-trace";
1314
import { getTokenInfoEffect } from "./auth";
1415
import { type Environment, envGetEffect, getApiUrl } from "./environment";
1516

@@ -76,7 +77,6 @@ const MAX_ERROR_SUMMARY_CHARS = 240;
7677
const MAX_ERROR_DEPTH = 6;
7778
const MAX_ERROR_ARRAY_ITEMS = 40;
7879
const MAX_ERROR_OBJECT_KEYS = 80;
79-
const DEFAULT_USER_AGENT = "godaddy-cli";
8080

8181
function findHeaderKey(
8282
headers: Record<string, string>,
@@ -108,7 +108,7 @@ function ensureRequiredRequestHeaders(headers: Record<string, string>): void {
108108
}
109109

110110
if (!hasNonEmptyHeader(headers, "user-agent")) {
111-
headers["user-agent"] = DEFAULT_USER_AGENT;
111+
headers["user-agent"] = CLI_USER_AGENT;
112112
}
113113
}
114114

src/core/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { Keychain } from "../effect/services/keychain";
1212
// handler (Promise context), outside the Effect runtime. The Fetch tag is for
1313
// code within Effect.gen contexts.
1414
import { loggedFetch } from "../services/logger";
15+
import { cliTraceHeaders } from "../shared/cli-trace";
1516
import {
1617
type Environment,
1718
envGetEffect,
@@ -183,6 +184,7 @@ export function authLoginEffect(options?: {
183184
method: "POST",
184185
headers: {
185186
"Content-Type": "application/x-www-form-urlencoded",
187+
...cliTraceHeaders(),
186188
},
187189
body: new URLSearchParams({
188190
client_id: clientId,

src/services/extension/upload.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,11 @@ export function uploadArtifactEffect(
9999
let lastError: Error | undefined;
100100

101101
for (let attempt = 1; attempt <= maxRetries; attempt++) {
102-
// Use only the requiredHeaders from the presigned URL (already signed)
103-
// Filter out x-amz-meta-upload-id as it's not signed
102+
// Presigned S3 PUT: send only the headers returned with the URL (they are
103+
// part of the AWS Signature v4 signing string). Do not add `user-agent` or
104+
// `x-request-id` here — unsigned headers can still affect behavior, and
105+
// anything not in the signature can break the upload. Traceability for
106+
// this hop is the presigned URL request (GraphQL), not the PUT itself.
104107
const { "x-amz-meta-upload-id": _, ...headers } = target.requiredHeaders;
105108

106109
logger.debug(

src/services/http-helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { Fetch } from "@effect/platform/FetchHttpClient";
66
import type { FileSystem } from "@effect/platform/FileSystem";
77
import * as Effect from "effect/Effect";
88
import { GraphQLClient } from "graphql-request";
9-
import { v7 as uuid } from "uuid";
109
import { type Environment, envGetEffect, getApiUrl } from "../core/environment";
1110
import { ConfigurationError } from "../effect/errors";
11+
import { cliTraceHeaders } from "../shared/cli-trace";
1212

1313
/**
1414
* Resolve the API base URL from environment variables or the active environment.
@@ -63,6 +63,6 @@ export function makeGraphQLClientEffect(): Effect.Effect<
6363
export function getRequestHeaders(accessToken: string): Record<string, string> {
6464
return {
6565
Authorization: `Bearer ${accessToken}`,
66-
"X-Request-ID": uuid(),
66+
...cliTraceHeaders(),
6767
};
6868
}

src/services/webhook-events.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
NetworkError,
99
type ValidationError,
1010
} from "../effect/errors";
11+
import { cliTraceHeaders } from "../shared/cli-trace";
1112
import { logHttpRequest, logHttpResponse } from "./logger";
1213

1314
export type WebhookEventType = {
@@ -47,7 +48,7 @@ export function getWebhookEventsTypesEffect({
4748
const url = `${baseUrl}/v1/apis/webhook-event-types`;
4849
const headers = {
4950
Authorization: `Bearer ${accessToken}`,
50-
UserAgent: "@godaddy/cli",
51+
...cliTraceHeaders(),
5152
};
5253

5354
const startTime = Date.now();

src/shared/cli-trace.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { v7 as uuid } from "uuid";
2+
import packageJson from "../../package.json";
3+
4+
/** Shared User-Agent for outbound CLI HTTP requests (server-side tracing). */
5+
export const CLI_USER_AGENT = `godaddy-cli/${packageJson.version}`;
6+
7+
/**
8+
* Headers attached to outbound requests so upstream logs can correlate calls.
9+
* Each invocation generates a fresh request ID.
10+
*
11+
* Keys are lowercase (`user-agent`, `x-request-id`) so the same spelling is
12+
* used as in `ensureRequiredRequestHeaders` (`src/core/api.ts`) and across
13+
* REST, GraphQL, OAuth, and webhook calls. HTTP treats header names as
14+
* case-insensitive; lowercase keeps log pipelines and tests consistent.
15+
*/
16+
export function cliTraceHeaders(): Record<string, string> {
17+
return {
18+
"user-agent": CLI_USER_AGENT,
19+
"x-request-id": uuid(),
20+
};
21+
}

tests/unit/core/api.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe("API Core Functions", () => {
8282
headers: expect.objectContaining({
8383
Authorization: "Bearer test-token-123",
8484
"x-request-id": expect.any(String),
85-
"user-agent": "godaddy-cli",
85+
"user-agent": expect.stringMatching(/^godaddy-cli\/\d+\.\d+\.\d+$/),
8686
}),
8787
}),
8888
);

tests/unit/services/extension/presigned-url.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ vi.mock("@/services/http-helpers", () => {
2727
return {
2828
getRequestHeaders: (token: string) => ({
2929
Authorization: `Bearer ${token}`,
30-
"X-Request-ID": "test-uuid",
30+
"user-agent": "godaddy-cli/0.0.0-test",
31+
"x-request-id": "test-uuid",
3132
}),
3233
makeGraphQLClientEffect: () =>
3334
Effect.succeed({
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, test } from "vitest";
2+
import packageJson from "../../../package.json";
3+
import { getRequestHeaders } from "../../../src/services/http-helpers";
4+
import { CLI_USER_AGENT, cliTraceHeaders } from "../../../src/shared/cli-trace";
5+
6+
describe("cli-trace", () => {
7+
test("CLI_USER_AGENT includes package version", () => {
8+
expect(CLI_USER_AGENT).toBe(`godaddy-cli/${packageJson.version}`);
9+
});
10+
11+
test("cliTraceHeaders sets lowercase user-agent and x-request-id", () => {
12+
const h = cliTraceHeaders();
13+
expect(h["user-agent"]).toBe(CLI_USER_AGENT);
14+
const rid = h["x-request-id"];
15+
expect(rid).toBeDefined();
16+
// Standard UUID string forms are 32 hex digits (with optional hyphens);
17+
// avoid a strict 8-4-4-4-12 regex so a future uuid package output shape
18+
// does not flake the test as long as it remains 32 hex.
19+
const hex = rid.replace(/-/g, "");
20+
expect(hex).toHaveLength(32);
21+
expect(hex).toMatch(/^[0-9a-f]+$/i);
22+
});
23+
24+
test("getRequestHeaders merges auth with trace headers", () => {
25+
const h = getRequestHeaders("tok");
26+
expect(h.Authorization).toBe("Bearer tok");
27+
expect(h["user-agent"]).toBe(CLI_USER_AGENT);
28+
expect(h["x-request-id"]).toBeTruthy();
29+
});
30+
});

0 commit comments

Comments
 (0)