Skip to content
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
2 changes: 1 addition & 1 deletion apps/api/src/auth/services/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class AuthInterceptor implements HonoInterceptor {

return await next();
} catch (error) {
this.logger.error(error);
this.logger.warn({ event: "API_KEY_VALIDATION_FAILED", message: error instanceof Error ? error.message : "Unknown error" });
throw new Unauthorized("Invalid API key");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const TrendIndicator = <Field extends string & Keys<Data>, Data extends H
if (firstValue === 0) return null;

const percentageChange = ((lastValue - firstValue) / firstValue) * 100;
const isCurrentDay = isToday(new Date(lastItem.date));
const isCurrentDay = isToday(new Date(`${lastItem.date}T00:00:00`));

return {
change: Math.round(percentageChange * 100) / 100,
Expand Down
3 changes: 2 additions & 1 deletion apps/deploy-web/src/pages/api/auth/password-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export default defineApiHandler({
const { cause, ...errorDetails } = result.val;
services.logger.warn({
event: "PASSWORD_LOGIN_ERROR",
cause: result.val
code: errorDetails.code,
message: errorDetails.message
});
return res.status(400).json(errorDetails);
}
Expand Down
3 changes: 2 additions & 1 deletion apps/deploy-web/src/pages/api/auth/password-signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export default defineApiHandler({
const { cause, ...errorDetails } = result.val;
services.logger.warn({
event: "PASSWORD_SIGNUP_ERROR",
cause: result.val
code: errorDetails.code,
message: errorDetails.message
});

if (result.val.code === "user_exists") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export default defineApiHandler({
const { cause, ...errorDetails } = result.val;
services.logger.warn({
event: "SEND_PASSWORD_RESET_EMAIL_ERROR",
cause: result.val
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue(blocking): without the cause we will not know the underlying issue. SO, when you receive this error in grafana, you won't be able to act.

Copy link
Copy Markdown
Contributor

@stalniy stalniy Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should tackle this from a different angle. Like understand what sensitive information auth0 errors can return (this would be weird IMO) and then redact it:

code: errorDetails.code,
message: errorDetails.message
});
return res.status(400).json(errorDetails);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,51 @@ describe(ErrorHandlerService.name, () => {
});
});

it("filters out sensitive headers from HTTP error response", () => {
const captureException = vi.fn().mockReturnValue("event-id-3");
const errorHandler = setup({ captureException });

const config = {
method: "post",
url: "https://api.example.com/auth"
} as InternalAxiosRequestConfig;
const httpError = new AxiosError(
"Request failed",
"401",
config,
{},
{
status: 401,
statusText: "Unauthorized",
headers: {
"content-type": "application/json",
"set-cookie": "session=secret-token; HttpOnly",
authorization: "Bearer secret-jwt",
server: "nginx"
},
data: {},
config: config
}
);

errorHandler.reportError({ error: httpError });

expect(captureException).toHaveBeenCalledWith(httpError, {
level: "error",
extra: {
headers: {
"content-type": "application/json",
server: "nginx"
}
},
tags: {
status: "401",
method: "POST",
url: "https://api.example.com/auth"
}
});
});

describe("wrapCallback", () => {
it("wraps synchronous function and reports error", () => {
const captureException = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class ErrorHandlerService {
finalTags.status = error.response.status.toString();
finalTags.method = error.response.config.method?.toUpperCase() || "UNKNOWN";
finalTags.url = error.response.config.url || "UNKNOWN";
extra.headers = error.response.headers;
extra.headers = pickSafeHeaders(error.response.headers);
}

this.logger.error({ ...extra, ...finalTags, error });
Expand Down Expand Up @@ -81,6 +81,18 @@ export interface TraceData {
baggage?: string;
}

const SAFE_HEADERS = new Set(["content-type", "content-length", "x-request-id", "x-correlation-id", "retry-after", "server"]);

function pickSafeHeaders(headers: Record<string, unknown>): Record<string, unknown> {
const safe: Record<string, unknown> = {};
for (const key of Object.keys(headers)) {
if (SAFE_HEADERS.has(key.toLowerCase())) {
safe[key] = headers[key];
}
}
return safe;
}

/**
* Converts Sentry `sentry-trace` header value into a valid W3C `traceparent`.
*
Expand Down
1 change: 0 additions & 1 deletion apps/deploy-web/src/services/session/session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ function extractResponseDetails(response: AxiosResponse) {
url: response.config?.url,
method: response.config?.method,
status: response.status,
data: response.data,
headers: {
"Content-Type": response.headers["content-type"],
server: response.headers["server"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ type HttpRequestLog = {
userId?: string;
};

function stripQueryParams(url: string): string {
const index = url.indexOf("?");
return index === -1 ? url : url.slice(0, index);
}

export class HttpLoggerInterceptor {
constructor(private readonly logger?: LoggerService) {}

Expand All @@ -34,7 +39,7 @@ export class HttpLoggerInterceptor {
const log: HttpRequestLog = {
httpRequest: {
requestMethod: c.req.method,
requestUrl: c.req.url,
requestUrl: stripQueryParams(c.req.url),
status: c.res.status,
referrer: c.req.raw.referrer,
protocol: c.req.header("x-forwarded-proto"),
Expand Down
62 changes: 53 additions & 9 deletions packages/logging/src/services/logger/logger.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ describe("LoggerService", () => {
level: expect.any(Function)
},
serializers: expect.any(Object),
redact: {
paths: expect.arrayContaining(["*.password", "*.access_token", "*.refresh_token"]),
censor: "[REDACTED]"
},
browser: {
formatters: {
level: expect.any(Function)
Expand Down Expand Up @@ -119,7 +123,7 @@ describe("LoggerService", () => {
status: 404,
message: "Not found",
stack: "stack trace",
data: { key: "value" }
data: { errorCode: "not_found", errorType: "client_error", secret: "should-be-filtered" }
});

logger.info(httpError);
Expand All @@ -129,7 +133,7 @@ describe("LoggerService", () => {
status: 404,
message: "Not found",
stack: "stack trace",
data: { key: "value" }
data: { errorCode: "not_found", errorType: "client_error" }
})
})
);
Expand All @@ -141,7 +145,7 @@ describe("LoggerService", () => {
status: 404,
message: "Not found",
stack: "stack trace",
data: { key: "value" },
data: { errorCode: "not_found", errorType: "client_error" },
originalError: new Error("Original error"),
cause: new Error("Cause error")
});
Expand All @@ -153,7 +157,7 @@ describe("LoggerService", () => {
status: 404,
message: "Not found",
stack: expect.stringContaining("stack trace\n\nCaused by:\n Error: Cause error"),
data: { key: "value" },
data: { errorCode: "not_found", errorType: "client_error" },
originalError: expect.stringContaining("Error: Original error")
})
})
Expand All @@ -166,7 +170,7 @@ describe("LoggerService", () => {
status: 404,
message: "Not found",
stack: "stack trace",
data: { key: "value" },
data: { errorCode: "not_found" },
originalError: new Error("Original error")
});

Expand All @@ -177,7 +181,7 @@ describe("LoggerService", () => {
status: 404,
message: "Not found",
stack: "stack trace",
data: { key: "value" },
data: { errorCode: "not_found", errorType: undefined },
originalError: expect.stringContaining("Error: Original error")
})
})
Expand Down Expand Up @@ -294,21 +298,61 @@ describe("LoggerService", () => {
);
});

it("should collect sql from error", () => {
it("should collect sql from error with redacted literals", () => {
const { logger, logs } = setup();
const error = new Error("Test error");
Object.assign(error, { sql: "SELECT * FROM users" });
Object.assign(error, { sql: "SELECT * FROM users WHERE name = 'John' AND id = 1234567" });
logger.info(error);
expect(logs[0]).toEqual(
expect.objectContaining({
err: expect.objectContaining({
sql: "SELECT * FROM users",
sql: "SELECT * FROM users WHERE name = '[REDACTED]' AND id = [REDACTED]",
stack: expect.stringContaining("Test error")
})
})
);
});

describe("redaction", () => {
it("should redact sensitive fields like password and tokens", () => {
const { logger, logs } = setup();
logger.info({ event: "TEST", credentials: { password: "s3cret", access_token: "tok_123", name: "safe" } });
expect(logs[0]).toEqual(
expect.objectContaining({
credentials: expect.objectContaining({
password: "[REDACTED]",
access_token: "[REDACTED]",
name: "safe"
})
})
);
});

it("should redact authorization and cookie headers", () => {
const { logger, logs } = setup();
logger.info({ event: "TEST", headers: { authorization: "Bearer secret", "set-cookie": "sid=abc", "content-type": "application/json" } });
expect(logs[0]).toEqual(
expect.objectContaining({
headers: expect.objectContaining({
authorization: "[REDACTED]",
"set-cookie": "[REDACTED]",
"content-type": "application/json"
})
})
);
});

it("should not redact non-sensitive fields", () => {
const { logger, logs } = setup();
logger.info({ event: "TEST", data: { status: 200, url: "/api/test" } });
expect(logs[0]).toEqual(
expect.objectContaining({
data: { status: 200, url: "/api/test" }
})
);
});
});

function setup() {
const logs: unknown[] = [];
const logger = new LoggerService({
Expand Down
34 changes: 32 additions & 2 deletions packages/logging/src/services/logger/logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,32 @@ export class LoggerService implements Logger {
msg: sanitizeString,
message: sanitizeString
},
redact: {
paths: [
"*.password",
"*.access_token",
"*.refresh_token",
"*.id_token",
"*.token",
"*.accessToken",
"*.refreshToken",
"*.idToken",
"*.client_secret",
"*.clientSecret",
"*.authorization",
"*.Authorization",
'*["set-cookie"]',
'*["Set-Cookie"]',
"*.cookie",
"*.Cookie",
"*.apiKey",
"*.api_key",
'*["x-api-key"]',
"cause.data",
"*.cause.data"
],
censor: "[REDACTED]"
},
...additionalOptions,
browser: {
formatters,
Expand Down Expand Up @@ -182,19 +208,23 @@ function logError(error: Error | undefined | null) {
status: error.status,
message: sanitizeString(error.message),
stack: collectFullErrorStack(error),
data: error.data,
data: error.data ? { errorCode: error.data.errorCode, errorType: error.data.errorType } : undefined,
originalError: error.originalError ? collectFullErrorStack(error.originalError) : undefined
};
}

if (Object.hasOwn(error, "sql")) {
return {
stack: collectFullErrorStack(error),
sql: (error as Error & { sql: string }).sql
sql: redactSqlLiterals((error as Error & { sql: string }).sql)
};
}

return collectFullErrorStack(error);
}

function redactSqlLiterals(sql: string): string {
return sql.replace(/'(?:[^'\\]|\\.)*'/g, "'[REDACTED]'").replace(/\b\d{6,}\b/g, "[REDACTED]");
}

declare let window: unknown;
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ export function collectFullErrorStack(error: string | Error | AggregateError | E
const requestedPath = currentError.response.config?.url
? `${currentError.response.config.method?.toUpperCase() || "(HTTP method not specified)"} ${currentError.response.config?.url}`
: "Unknown request";
Comment on lines 24 to 26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the target file to see the full context around lines 24-26
head -40 packages/logging/src/utils/collect-full-error-stack/collect-full-error-stack.ts | cat -n

Repository: akash-network/console

Length of output: 1898


🏁 Script executed:

# Search for query string stripping patterns in the logging package
rg -n 'split.*\?|stripQuery|removeQuery|URL.*query' packages/logging/ --type ts -C2

Repository: akash-network/console

Length of output: 1040


🏁 Script executed:

# Check for any URL sanitization utilities in the codebase
fd -e ts -e js packages/logging/ | xargs grep -l "sanitize.*url\|stripQuery\|removeQuery" 2>/dev/null || echo "No sanitization utils found"

Repository: akash-network/console

Length of output: 483


🏁 Script executed:

# Search broadly for URL query parameter handling in the entire codebase
rg -n 'split\("?\?\)' --type ts --type tsx | head -20

Repository: akash-network/console

Length of output: 92


🏁 Script executed:

# Check if there are any PR objectives or documentation files
find . -maxdepth 3 -name "*.md" -o -name "OBJECTIVES*" -o -name "PHASE*" | head -10

Repository: akash-network/console

Length of output: 441


🏁 Script executed:

# Check if stripQueryParams is exported from http-logger.service.ts
cat -n packages/logging/src/hono/http-logger/http-logger.service.ts | head -50

Repository: akash-network/console

Length of output: 1729


🏁 Script executed:

# Check if there's a shared utils or utilities directory for logging functions
fd -type f -name "*.ts" packages/logging/src/utils/ | head -20

Repository: akash-network/console

Length of output: 235


🏁 Script executed:

# Look for any exports or re-exports in the logging package index files
find packages/logging/src -name "index.ts" -o -name "index.js" | xargs cat 2>/dev/null

Repository: akash-network/console

Length of output: 311


🏁 Script executed:

# Check git history/context - when was collect-full-error-stack added vs http-logger
git log --oneline --all -- packages/logging/src/utils/collect-full-error-stack/collect-full-error-stack.ts packages/logging/src/hono/http-logger/http-logger.service.ts 2>/dev/null | head -10

Repository: akash-network/console

Length of output: 744


URL query strings should be stripped before logging to prevent sensitive data exposure.

Lines 24-26 log the URL as-is, which may contain sensitive query parameters like tokens or API keys. A stripQueryParams() pattern already exists in the same logging package (packages/logging/src/hono/http-logger/http-logger.service.ts), confirming this is an expected practice—apply the same sanitization here for consistency.

♻️ Suggested implementation
-    const requestedPath = currentError.response.config?.url
-      ? `${currentError.response.config.method?.toUpperCase() || "(HTTP method not specified)"} ${currentError.response.config?.url}`
+    const rawUrl = currentError.response.config?.url;
+    const sanitizedUrl = rawUrl ? rawUrl.split("?")[0] : undefined;
+    const requestedPath = sanitizedUrl
+      ? `${currentError.response.config.method?.toUpperCase() || "(HTTP method not specified)"} ${sanitizedUrl}`
       : "Unknown request";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requestedPath = currentError.response.config?.url
? `${currentError.response.config.method?.toUpperCase() || "(HTTP method not specified)"} ${currentError.response.config?.url}`
: "Unknown request";
const rawUrl = currentError.response.config?.url;
const sanitizedUrl = rawUrl ? rawUrl.split("?")[0] : undefined;
const requestedPath = sanitizedUrl
? `${currentError.response.config.method?.toUpperCase() || "(HTTP method not specified)"} ${sanitizedUrl}`
: "Unknown request";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/logging/src/utils/collect-full-error-stack/collect-full-error-stack.ts`
around lines 24 - 26, The logged request URL in the requestedPath construction
leaks query strings; update the code that builds requestedPath (in
collect-full-error-stack.ts where currentError.response.config?.url is used) to
sanitize the URL by importing and applying the existing stripQueryParams utility
(the same function used in http-logger.service.ts) so that you log the HTTP
method plus stripQueryParams(currentError.response.config.url) (or "Unknown
request" when absent); ensure you still fall back to "(HTTP method not
specified)" for missing methods.

const body = currentError.response.data ? JSON.stringify(currentError.response.data) : "No body";
stack.push(
"\nResponse:",
`\nRequest: ${requestedPath}`,
`\nStatus: ${currentError.response.status}`,
`\nError: ${sanitizeString(currentError.response.data?.message) || "Not specified"} (code: ${currentError.response.data?.code || "Not specified"})`,
`\nBody: ${body.length > 200 ? `${body.slice(0, 200)}...` : body}`
`\nError: ${sanitizeString(currentError.response.data?.message) || "Not specified"} (code: ${currentError.response.data?.code || "Not specified"})`
);
}

Expand Down
Loading