Skip to content

Add send logs#648

Merged
Doezer merged 3 commits into
release/1.4.0from
feat/send-logs-button-using-external-worker
Jun 1, 2026
Merged

Add send logs#648
Doezer merged 3 commits into
release/1.4.0from
feat/send-logs-button-using-external-worker

Conversation

@Doezer
Copy link
Copy Markdown
Owner

@Doezer Doezer commented May 31, 2026

This pull request introduces a new SendLogsDialog component for uploading logs to support, along with comprehensive tests for both the dialog and the underlying log upload utilities. The changes ensure that logs are scrubbed of PII, uploaded securely, and that users receive clear feedback and guidance throughout the process. The most important changes are summarized below.

New Feature: Send Logs Dialog

  • Added the SendLogsDialog component (client/src/components/SendLogsDialog.tsx) for uploading logs to support, including UI for consent, upload progress, success (with support code and GitHub issue link), and error handling. The dialog scrubs PII from logs, detects platform, and provides user feedback via toasts and UI states.

Testing: Dialog and Log Upload Utilities

  • Added a full test suite for the SendLogsDialog component (client/__tests__/SendLogsDialog.test.tsx), covering log submission, error and rate-limit handling, clipboard copy feedback, and UI state transitions.
  • Added a test suite for log upload utilities (client/__tests__/send-logs.test.ts), verifying PII scrubbing, log submission, error handling, GitHub issue URL generation, and platform detection.

Test Improvements and Fixes

  • Improved an existing test in LogsPage.remaining.test.tsx to ensure log streaming events are handled inside act for proper React state updates.
  • Added act import to LogsPage.remaining.test.tsx to support the above fix.

@Doezer Doezer added this to Questarr May 31, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Questarr May 31, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a support log submission feature, allowing users to scrub and upload their server logs to a private repository. It adds the SendLogsDialog component, PII scrubbing utilities, Cloudflare Worker integration, and comprehensive tests. The code review highlights several critical issues and improvement opportunities: the IPv6 validation regex incorrectly matches standard timestamps and fails on compressed addresses; potential JSON parsing failures in sendLogs could leave the UI stuck; navigator.clipboard needs a guard to prevent crashes in non-secure contexts; and passing a date generator prop would improve component purity and testability.

Comment on lines +79 to +89
function isIpv6(value: string): boolean {
if (!value.includes(":")) return false;

const compressedGroups = value.split("::");
if (compressedGroups.length > 2) return false;

const groups = value.split(":");
if (groups.length < 3 || groups.length > 8) return false;

return groups.every((group) => group === "" || /^[0-9a-fA-F]{1,4}$/.test(group));
}
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.

high

The current isIpv6 implementation incorrectly identifies standard HH:MM:SS timestamps (e.g., 12:34:56) as valid IPv6 addresses because they contain 3 groups of valid hex characters separated by colons. This leads to timestamps in the logs being erroneously replaced with [ip]. To fix this, we should enforce that an IPv6 address without a double colon (::) must have exactly 8 groups.

Suggested change
function isIpv6(value: string): boolean {
if (!value.includes(":")) return false;
const compressedGroups = value.split("::");
if (compressedGroups.length > 2) return false;
const groups = value.split(":");
if (groups.length < 3 || groups.length > 8) return false;
return groups.every((group) => group === "" || /^[0-9a-fA-F]{1,4}$/.test(group));
}
function isIpv6(value: string): boolean {
if (!value.includes(":")) return false;
const compressedGroups = value.split("::");
if (compressedGroups.length > 2) return false;
const groups = value.split(":");
if (groups.length < 3 || groups.length > 8) return false;
// If not compressed, it must have exactly 8 groups to be a valid IPv6 address
if (compressedGroups.length === 1 && groups.length !== 8) return false;
return groups.every((group) => group === "" || /^[0-9a-fA-F]{1,4}$/.test(group));
}

Comment on lines +114 to +158
export async function sendLogs(payload: SendLogsPayload): Promise<SendLogsResult> {
if (SUPPORT_WORKER_URL.includes("REPLACE_ME")) {
return {
ok: false,
status: 0,
message: "Log upload is not configured for this build.",
};
}

let response: Response;
try {
response = await fetch(SUPPORT_WORKER_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
} catch (err) {
return {
ok: false,
status: 0,
message: err instanceof Error ? err.message : "Network error — check your connection.",
};
}

if (response.ok) {
const data = (await response.json()) as { code: string; issueNumber: number };
return { ok: true, code: data.code, issueNumber: data.issueNumber };
}

const errorMessages: Record<number, string> = {
413: "Log payload is too large (> 500 KB). Try clearing old logs first.",
429: "Rate limit reached (5 submissions per hour). Try again later.",
502: "Log server could not reach GitHub. Try again in a moment.",
};

let message = errorMessages[response.status] ?? `Unexpected error (HTTP ${response.status}).`;
try {
const body = (await response.json()) as { error?: string };
if (body.error) message = body.error;
} catch {
// ignore — use the default message
}

return { ok: false, status: response.status, message };
}
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.

high

If response.json() fails on the success path (e.g., due to an empty or invalid JSON response from the server), it will throw an unhandled exception and reject the promise returned by sendLogs. Since the caller handleSend does not wrap sendLogs in a try/catch, this will leave the dialog stuck in the "sending" state forever. Wrapping the entire body of sendLogs in a try/catch block ensures any unexpected errors or parsing failures are caught and returned gracefully as a failure result.

export async function sendLogs(payload: SendLogsPayload): Promise<SendLogsResult> {
  if (SUPPORT_WORKER_URL.includes("REPLACE_ME")) {
    return {
      ok: false,
      status: 0,
      message: "Log upload is not configured for this build.",
    };
  }

  try {
    const response = await fetch(SUPPORT_WORKER_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });

    if (response.ok) {
      const data = (await response.json()) as { code: string; issueNumber: number };
      return { ok: true, code: data.code, issueNumber: data.issueNumber };
    }

    const errorMessages: Record<number, string> = {
      413: "Log payload is too large (> 500 KB). Try clearing old logs first.",
      429: "Rate limit reached (5 submissions per hour). Try again later.",
      502: "Log server could not reach GitHub. Try again in a moment.",
    };

    let message = errorMessages[response.status] ?? `Unexpected error (HTTP ${response.status}).`;
    try {
      const body = (await response.json()) as { error?: string };
      if (body.error) message = body.error;
    } catch {
      // ignore — use the default message
    }

    return { ok: false, status: response.status, message };
  } catch (err) {
    return {
      ok: false,
      status: 0,
      message: err instanceof Error ? err.message : "Network error — check your connection.",
    };
  }
}

Comment thread client/src/lib/send-logs.ts Outdated
const IPV4_RE = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;

/** IPv6 candidates are validated after matching to avoid an overly complex regex */
const IPV6_RE = /\b[A-Fa-f0-9:]{2,}\b/g;
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.

medium

Using \b (word boundary) with a pattern that can start or end with a colon : (like IPv6 addresses ::1 or fe80::) prevents matching when the address is preceded or followed by non-word characters (such as spaces or equal signs). Replacing \b with lookarounds ensures compressed IPv6 addresses starting or ending with colons are correctly matched and scrubbed.

Suggested change
const IPV6_RE = /\b[A-Fa-f0-9:]{2,}\b/g;
const IPV6_RE = /(?<![A-Fa-f0-9:])[A-Fa-f0-9:]{2,}(?![A-Fa-f0-9:])/g;

Comment on lines +21 to +26
interface SendLogsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** Raw NDJSON lines currently visible in the log viewer */
logLines: string[];
}
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.

medium

To ensure component purity and testability, pass non-deterministic values like new Date() as props rather than creating them directly within the component. Adding an optional getCurrentDate prop allows the parent component or tests to inject a deterministic date generator.

Suggested change
interface SendLogsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** Raw NDJSON lines currently visible in the log viewer */
logLines: string[];
}
interface SendLogsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** Raw NDJSON lines currently visible in the log viewer */
logLines: string[];
/** Optional function to get the current date, useful for testing and purity */
getCurrentDate?: () => Date;
}
References
  1. To ensure component purity and testability, pass non-deterministic values like new Date() as props rather than creating them directly within the component.

Comment on lines +52 to +65
const handleSend = useCallback(async () => {
setStep("sending");

const scrubbedLogs = scrubLogLines(logLines);
const outcome = await sendLogs({
logs: scrubbedLogs,
appVersion: APP_VERSION,
platform: detectPlatform(),
timestamp: new Date().toISOString(),
});

setResult(outcome);
setStep(outcome.ok ? "success" : "error");
}, [logLines]);
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.

medium

Use the optional getCurrentDate prop to obtain the current timestamp instead of hardcoding new Date(), adhering to the component purity and testability guidelines.

Suggested change
const handleSend = useCallback(async () => {
setStep("sending");
const scrubbedLogs = scrubLogLines(logLines);
const outcome = await sendLogs({
logs: scrubbedLogs,
appVersion: APP_VERSION,
platform: detectPlatform(),
timestamp: new Date().toISOString(),
});
setResult(outcome);
setStep(outcome.ok ? "success" : "error");
}, [logLines]);
const handleSend = useCallback(async () => {
setStep("sending");
const scrubbedLogs = scrubLogLines(logLines);
const outcome = await sendLogs({
logs: scrubbedLogs,
appVersion: APP_VERSION,
platform: detectPlatform(),
timestamp: (getCurrentDate ? getCurrentDate() : new Date()).toISOString(),
});
setResult(outcome);
setStep(outcome.ok ? "success" : "error");
}, [logLines, getCurrentDate]);
References
  1. To ensure component purity and testability, pass non-deterministic values like new Date() as props rather than creating them directly within the component.

Comment on lines +67 to +81
const handleCopyCode = useCallback(() => {
if (!result?.ok) return;
navigator.clipboard
.writeText(result.code)
.then(() => {
toast({ title: "Copied", description: `Code ${result.code} copied to clipboard` });
})
.catch(() => {
toast({
title: "Copy failed",
description: "Clipboard access denied",
variant: "destructive",
});
});
}, [result, toast]);
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.

medium

navigator.clipboard can be undefined in older browsers or in non-secure contexts (HTTP). Attempting to call writeText on it directly will throw a TypeError and crash the component. Adding a guard prevents this runtime error and allows showing a helpful fallback toast.

  const handleCopyCode = useCallback(() => {
    if (!result?.ok) return;
    if (!navigator.clipboard) {
      toast({
        title: "Copy failed",
        description: "Clipboard API not supported in this browser",
        variant: "destructive",
      });
      return;
    }
    navigator.clipboard
      .writeText(result.code)
      .then(() => {
        toast({ title: "Copied", description: `Code ${result.code} copied to clipboard` });
      })
      .catch(() => {
        toast({
          title: "Copy failed",
          description: "Clipboard access denied",
          variant: "destructive",
        });
      });
  }, [result, toast]);

@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

Codecov Report

❌ Patch coverage is 96.81529% with 5 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
client/src/lib/send-logs.ts 97.02% 3 Missing ⚠️
client/src/components/SendLogsDialog.tsx 97.95% 1 Missing ⚠️
client/src/pages/logs.tsx 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5da70243b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/lib/support-config.ts Outdated
* placeholder below with the URL printed by Wrangler, e.g.:
* https://questarr-log-collector.<your-subdomain>.workers.dev
*/
export const SUPPORT_WORKER_URL = "https://questarr-log-collector.questarr.workers.dev";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow the support worker in the CSP

When the dialog calls this hardcoded Cloudflare Worker, the browser is still governed by the app's Helmet CSP; I checked server/routes.ts and connect-src only allows 'self', https://raw.githubusercontent.com, and https://api.github.com (plus websockets in non-production). In production (and dev), fetch("https://questarr-log-collector.questarr.workers.dev") will be blocked by CSP before reaching the worker, so the new Send Logs flow cannot upload logs until this origin is added to connect-src or the request is proxied through the app.

Useful? React with 👍 / 👎.

return text
.replace(JWT_RE, "[jwt]") // before email — JWTs contain dots
.replace(EMAIL_RE, "[email]")
.replace(IPV6_RE, (match) => (isIpv6(match) ? "[ip]" : match))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid scrubbing clock times as IPv6 addresses

For unstructured log lines or messages that contain plain clock times like 12:34:56, this IPv6 pass treats the value as a valid three-group IPv6 candidate and replaces it with [ip]. That strips useful timing information from the support bundle even though no IP address was present; the IPv6 detection should require a real IPv6 shape rather than accepting any colon-separated hex groups.

Useful? React with 👍 / 👎.

Comment thread client/__tests__/SendLogsDialog.test.tsx Fixed
Comment thread client/__tests__/SendLogsDialog.test.tsx Fixed
Comment thread client/__tests__/send-logs.test.ts Fixed
@Doezer Doezer force-pushed the feat/send-logs-button-using-external-worker branch from 774fc27 to 3e73b78 Compare June 1, 2026 11:25
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 1, 2026

@Doezer Doezer merged commit 91f3511 into release/1.4.0 Jun 1, 2026
6 checks passed
@Doezer Doezer deleted the feat/send-logs-button-using-external-worker branch June 1, 2026 11:38
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Questarr Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants