Skip to content

feat(desktop): Session Profile sharing in Settings → Feedback#1941

Open
richiemcilroy wants to merge 6 commits into
mainfrom
cursor/session-profiling-feedback-6720
Open

feat(desktop): Session Profile sharing in Settings → Feedback#1941
richiemcilroy wants to merge 6 commits into
mainfrom
cursor/session-profiling-feedback-6720

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Jun 21, 2026

Copy link
Copy Markdown
Member

Overview

Adds a Session Profiling system to Settings → Feedback so a user can hand us everything we need to fully understand, reproduce and fix a bug from their session in one click.

When triggered, the desktop app:

  1. Finds the user's latest Studio mode recording and latest Instant mode recording.
  2. Bundles the full recording directories (raw segments, audio, cursor/keyboard data, project config, render output, screenshots — everything) together with full system diagnostics (diagnostics.json), a profile summary (profile.json), and the latest log file into a single zip.
  3. Requests a presigned S3 URL, uploads the bundle to S3, then posts the download link + a diagnostics summary to our Discord feedback channel.

Behavior per the request:

  • If both Studio and Instant recordings exist → both are bundled.
  • If only one exists → only that one is bundled.
  • If neither exists → the UI prompts the user to record one first (no upload).

Walkthrough

Populated state (latest Studio + Instant recordings listed, with sizes/dates, optional note, and Send button):

Share a Session Profile populated state

Empty state (prompts the user to record first):

Share a Session Profile empty state

Rust test results (incl. end-to-end upload→S3→notify round-trip against a mock server):

session_profile_tests.log

Changes

Desktop (Rust)apps/desktop/src-tauri/src/session_profile.rs (new)

  • find_latest_recordings discovers the newest Studio/Instant .cap bundles.
  • build_profile_zip streams each recording dir + diagnostics + summary + log into a zip (stored for media, deflate for text; zip64 for large files).
  • Streams the bundle to a presigned S3 URL and calls the notify endpoint; emits a SessionProfileProgress event for live UI progress.
  • New commands: get_session_profile_status, upload_session_profile.
  • logging.rs exposes collect_diagnostics_for_upload + a new summarize_diagnostics (Markdown summary for Discord) + get_latest_log_file. web_api.rs exposes apply_env_headers.

Webapps/web/app/api/desktop/[...route]/root.ts

  • POST /api/desktop/session-profile/create → presigned S3 PUT URL scoped to desktop-session-profiles/{userId}/{id}/.
  • POST /api/desktop/session-profile/notify → validates the key belongs to the user, generates a 7‑day presigned download URL, and posts the link + diagnostics summary to DISCORD_FEEDBACK_WEBHOOK_URL.

Desktop (UI)apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx

  • New "Share a Session Profile" section: lists the recordings to include, an optional note, a progress bar, and a Send button; shows the empty-state prompt when there are no recordings.

Generated tauri.ts bindings updated for the new commands/event/types.

Testing

  • cargo test -p cap-desktop session_profile — 5 tests pass, including upload_and_notify_round_trip which builds a real zip and exercises create → S3 PUT → notify against a local mock server.
  • cargo build -p cap-desktop, cargo fmt --check, cargo clippy -p cap-desktop all clean.
  • Manually verified the UI in the running Linux desktop build (screenshots above): populated state lists Studio + Instant recordings; Refresh with no recordings shows the record-first prompt.

Note: the full upload→S3→Discord path can't be exercised from the GUI in this environment (requires sign-in + live S3 + Discord webhook), so it is covered by the end-to-end Rust round-trip test instead.

Open in Web Open in Cursor 

Greptile Summary

This PR adds a Session Profile sharing feature to Settings → Feedback: the desktop app bundles the user's latest Studio and Instant recordings with system diagnostics and logs into a zip, uploads it to S3 via a presigned URL, and posts a Discord notification with a download link.

  • Rust backend (session_profile.rs): discovers recordings, compresses them with correct zip64 support and media/text compression selection, enforces a 10 GB size guard, and streams the upload with live progress events.
  • Web API (root.ts): two new endpoints scope S3 keys to the authenticated user and post a Discord notification; key ownership is validated before generating a download URL.
  • Frontend (feedback.tsx): renders available recordings, an optional note textarea, and a progress bar; the Tauri event listener is correctly cleaned up via onCleanup.

Confidence Score: 4/5

Safe to merge after fixing the Discord message ordering in root.ts; all other paths are sound.

The notify endpoint places the user note before the download URL in the Discord message. A note exceeding ~1400 characters pushes the presigned download link past Discord's 2000-character cut-off, making the uploaded bundle unreachable by the team. Everything else — S3 key scoping, zip64 handling, size guard, temp-file cleanup, and frontend event-listener teardown — looks correct.

apps/web/app/api/desktop/[...route]/root.ts — the Discord message ordering around line 596 needs the download URL moved above the note.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/session_profile.rs New 1107-line module: discovers latest recordings, builds a zip bundle (with correct zip64, compression selection, and temp-file cleanup), and streams upload to S3 with live progress events. Size guard (10 GB) and note truncation are present. Clean overall; no blocking issues in the Rust layer.
apps/web/app/api/desktop/[...route]/root.ts Adds two new endpoints (create presigned PUT URL, notify via Discord). Key validation is user-scoped and sound. The Discord message places the user note before the download URL, so a note longer than ~1400 chars causes the 2000-char Discord limit to truncate the download link away — making the uploaded bundle unreachable by the team.
apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx New 'Share a Session Profile' section with progress bar and empty-state prompt. Event listener cleanup is correctly hoisted to component scope and called from onCleanup.
apps/desktop/src-tauri/src/logging.rs Exposes previously private helpers (get_latest_log_file, collect_diagnostics_for_upload, LogUploadDiagnostics) and adds summarize_diagnostics for Discord-formatted output. No issues.
apps/desktop/src/utils/tauri.ts Auto-generated bindings updated with new commands, event, and types. No issues.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/web/app/api/desktop/[...route]/root.ts:596-601
**Download URL disappears from Discord when note is long.** `note` appears before the download link in the message array. A note longer than ~1400 characters — well within the 4000-char schema and UI limit — pushes the download URL past Discord's 2000-character hard cut-off. Since the presigned URL is the only way for the team to retrieve the bundle (it is never displayed in the desktop UI), any profile submitted with a moderately detailed note becomes inaccessible. Moving the `**Download …**` line to appear before the note block guarantees it is always included.

Reviews (2): Last reviewed commit: "fix(web): treat session profile Discord ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

cursoragent and others added 3 commits June 21, 2026 12:10
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…S3 upload

Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
@richiemcilroy richiemcilroy marked this pull request as ready for review June 21, 2026 12:50
app: AppHandle,
note: Option<String>,
) -> Result<SessionProfileUploadResult, String> {
let note = note.and_then(|value| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks like /api/desktop/session-profile/notify enforces a 4000-char max for note. If the user pastes something long, this will fail after the bundle is uploaded.

Suggested change
let note = note.and_then(|value| {
let note = note.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
let mut note = trimmed.to_string();
note.truncate(4000);
Some(note)
}
});

</div>
</div>

<textarea

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since the server caps note at 4000 chars, it’d be nice to enforce the same in the UI to avoid a late notify failure.

Suggested change
<textarea
<textarea
value={profileNote()}
onInput={(e) => setProfileNote(e.currentTarget.value)}
disabled={sendingProfile()}
maxLength={4000}
placeholder="Optional: describe the bug or what you were doing when it happened..."
class="p-2 w-full h-24 text-[13px] rounded-md border transition-colors duration-200 resize-none bg-gray-2 placeholder:text-gray-10 border-gray-3 text-primary focus:outline-hidden focus:ring-1 focus:ring-gray-8 hover:border-gray-6 disabled:opacity-50"
/>

Comment on lines +622 to +629
if (!response.ok) {
throw new Error(
`Failed to send session profile to Discord: ${response.statusText}`,
);
}

discordDelivered = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If Discord delivery fails, throwing here makes the whole notify call a 500 even though the bundle is uploaded and the downloadUrl is valid. Consider treating Discord as best-effort so the client doesn’t retry/re-upload.

Suggested change
if (!response.ok) {
throw new Error(
`Failed to send session profile to Discord: ${response.statusText}`,
);
}
discordDelivered = true;
}
if (!response.ok) {
console.error(
"Failed to send session profile to Discord:",
response.status,
await response.text(),
);
} else {
discordDelivered = true;
}

Comment on lines +114 to +143
const handleSendProfile = async () => {
setSendingProfile(true);
setProfileResult(null);
setProfileProgress(null);

let unlisten: (() => void) | undefined;
try {
unlisten = await events.sessionProfileProgress.listen((event) =>
setProfileProgress(event.payload),
);
const note = profileNote().trim();
const result = await commands.uploadSessionProfile(note || null);
setProfileResult(result);
setProfileNote("");
toast.success("Session profile sent. Thank you!");
} catch (error) {
console.error("Failed to send session profile:", error);
toast.error(
typeof error === "string"
? error
: "Failed to send session profile. Please try again.",
);
} finally {
unlisten?.();
setProfileProgress(null);
setSendingProfile(false);
}
};

onCleanup(() => setProfileProgress(null));

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.

P1 Event listener not cleaned up on component unmount. The unlisten function is scoped inside handleSendProfile, so onCleanup cannot reach it. If the user navigates away from the Settings page while an upload is in progress, the sessionProfileProgress Tauri listener keeps firing and calls setProfileProgress on a disposed reactive root until the upload finally finishes.

Suggested change
const handleSendProfile = async () => {
setSendingProfile(true);
setProfileResult(null);
setProfileProgress(null);
let unlisten: (() => void) | undefined;
try {
unlisten = await events.sessionProfileProgress.listen((event) =>
setProfileProgress(event.payload),
);
const note = profileNote().trim();
const result = await commands.uploadSessionProfile(note || null);
setProfileResult(result);
setProfileNote("");
toast.success("Session profile sent. Thank you!");
} catch (error) {
console.error("Failed to send session profile:", error);
toast.error(
typeof error === "string"
? error
: "Failed to send session profile. Please try again.",
);
} finally {
unlisten?.();
setProfileProgress(null);
setSendingProfile(false);
}
};
onCleanup(() => setProfileProgress(null));
let currentUnlisten: (() => void) | undefined;
onCleanup(() => {
currentUnlisten?.();
currentUnlisten = undefined;
setProfileProgress(null);
});
const handleSendProfile = async () => {
setSendingProfile(true);
setProfileResult(null);
setProfileProgress(null);
try {
currentUnlisten = await events.sessionProfileProgress.listen((event) =>
setProfileProgress(event.payload),
);
const note = profileNote().trim();
const result = await commands.uploadSessionProfile(note || null);
setProfileResult(result);
setProfileNote("");
toast.success("Session profile sent. Thank you!");
} catch (error) {
console.error("Failed to send session profile:", error);
toast.error(
typeof error === "string"
? error
: "Failed to send session profile. Please try again.",
);
} finally {
currentUnlisten?.();
currentUnlisten = undefined;
setProfileProgress(null);
setSendingProfile(false);
}
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx
Line: 114-143

Comment:
Event listener not cleaned up on component unmount. The `unlisten` function is scoped inside `handleSendProfile`, so `onCleanup` cannot reach it. If the user navigates away from the Settings page while an upload is in progress, the `sessionProfileProgress` Tauri listener keeps firing and calls `setProfileProgress` on a disposed reactive root until the upload finally finishes.

```suggestion
	let currentUnlisten: (() => void) | undefined;

	onCleanup(() => {
		currentUnlisten?.();
		currentUnlisten = undefined;
		setProfileProgress(null);
	});

	const handleSendProfile = async () => {
		setSendingProfile(true);
		setProfileResult(null);
		setProfileProgress(null);

		try {
			currentUnlisten = await events.sessionProfileProgress.listen((event) =>
				setProfileProgress(event.payload),
			);
			const note = profileNote().trim();
			const result = await commands.uploadSessionProfile(note || null);
			setProfileResult(result);
			setProfileNote("");
			toast.success("Session profile sent. Thank you!");
		} catch (error) {
			console.error("Failed to send session profile:", error);
			toast.error(
				typeof error === "string"
					? error
					: "Failed to send session profile. Please try again.",
			);
		} finally {
			currentUnlisten?.();
			currentUnlisten = undefined;
			setProfileProgress(null);
			setSendingProfile(false);
		}
	};
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +439 to +447
let response = request
.send()
.await
.map_err(|err| format!("Failed to request upload URL: {err}"))?;

if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Upload URL request failed ({status}): {body}"));

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.

P2 TOCTOU race in TempFileGuard::drop: the file could be removed between the exists() check and remove_file(), causing a spurious warning. The idiomatic Rust pattern is to attempt the removal and ignore NotFound — this is both race-free and simpler.

Suggested change
let response = request
.send()
.await
.map_err(|err| format!("Failed to request upload URL: {err}"))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Upload URL request failed ({status}): {body}"));
impl Drop for TempFileGuard {
fn drop(&mut self) {
if let Err(err) = std::fs::remove_file(&self.0) {
if err.kind() != std::io::ErrorKind::NotFound {
warn!(error = %err, path = %self.0.display(), "Failed to clean up session profile bundle");
}
}
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/session_profile.rs
Line: 439-447

Comment:
TOCTOU race in `TempFileGuard::drop`: the file could be removed between the `exists()` check and `remove_file()`, causing a spurious warning. The idiomatic Rust pattern is to attempt the removal and ignore `NotFound` — this is both race-free and simpler.

```suggestion
impl Drop for TempFileGuard {
    fn drop(&mut self) {
        if let Err(err) = std::fs::remove_file(&self.0) {
            if err.kind() != std::io::ErrorKind::NotFound {
                warn!(error = %err, path = %self.0.display(), "Failed to clean up session profile bundle");
            }
        }
    }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +605 to +699
let mut recordings: Vec<SessionProfileRecording> = Vec::new();
if let Some(studio) = status.studio {
recordings.push(studio);
}
if let Some(instant) = status.instant {
recordings.push(instant);
}

if recordings.is_empty() {
return Err("No Studio or Instant recordings found to profile.".to_string());
}

let is_recording = {
let app_lock = app.state::<crate::ArcLock<crate::App>>();
let state = app_lock.read().await;
matches!(
state.recording_state,
crate::RecordingState::Active(_) | crate::RecordingState::Pending { .. }
)
};

let diagnostics =
logging::collect_diagnostics_for_upload(&recordings_dir, &app_data_dir, is_recording);
let diagnostics_json =
serde_json::to_string_pretty(&diagnostics).unwrap_or_else(|_| "{}".to_string());
let diagnostics_summary = logging::summarize_diagnostics(&diagnostics);

let profile_summary = build_profile_summary(&recordings, note.as_deref());
let profile_json =
serde_json::to_string_pretty(&profile_summary).unwrap_or_else(|_| "{}".to_string());

let log_file = logging::get_latest_log_file(app).await;

let file_name = format!(
"cap-session-profile-{}.zip",
chrono::Utc::now().format("%Y%m%d-%H%M%S")
);
let zip_path = std::env::temp_dir().join(format!("{}-{file_name}", uuid::Uuid::new_v4()));
let bundle_guard = TempFileGuard(zip_path.clone());

emit_progress(
app,
SessionProfileStage::Compressing,
0.0,
"Compressing recordings…",
);

let bundle_size = {
let app_progress = app.clone();
let zip_path = zip_path.clone();
let recordings = recordings.clone();
let diagnostics_json = diagnostics_json.clone();
let profile_json = profile_json.clone();
let log_file = log_file.clone();

tokio::task::spawn_blocking(move || {
let mut last_pct: i64 = -1;
build_profile_zip(
&zip_path,
&recordings,
&diagnostics_json,
&profile_json,
log_file.as_deref(),
|done, total| {
let pct = if total > 0 {
((done.saturating_mul(100)) / total) as i64
} else {
100
};
if pct != last_pct {
last_pct = pct;
emit_progress(
&app_progress,
SessionProfileStage::Compressing,
pct as f64 / 100.0,
"Compressing recordings…",
);
}
},
)
})
.await
.map_err(|err| format!("Bundle task failed: {err}"))??
};

let client = app
.state::<RetryableHttpClient>()
.as_ref()
.map_err(|err| format!("HTTP client unavailable: {err:?}"))?
.clone();
let base_url = app.make_app_url("").await;
let bearer = auth_bearer_token(app)?;

emit_progress(
app,

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.

P2 No bundle size guard before zipping or uploading. build_profile_zip streams entire recording directories into a temp file without any upper-bound check. A user with several large Studio recordings (each can easily be multiple GB) could fill /tmp and then attempt a multi-GB streaming PUT to the presigned S3 URL. There is no feedback to the user about estimated size before the operation starts, and no hard cap to protect disk space. Consider computing the total uncompressed size up-front (you already have recording.size_bytes) and either warning the UI or refusing uploads above a reasonable limit (e.g. 5–10 GB).

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/session_profile.rs
Line: 605-699

Comment:
**No bundle size guard before zipping or uploading.** `build_profile_zip` streams entire recording directories into a temp file without any upper-bound check. A user with several large Studio recordings (each can easily be multiple GB) could fill `/tmp` and then attempt a multi-GB streaming PUT to the presigned S3 URL. There is no feedback to the user about estimated size before the operation starts, and no hard cap to protect disk space. Consider computing the total uncompressed size up-front (you already have `recording.size_bytes`) and either warning the UI or refusing uploads above a reasonable limit (e.g. 5–10 GB).

How can I resolve this? If you propose a fix, please make it concise.

cursoragent and others added 3 commits June 21, 2026 14:13
…nd cap note length

Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…n temp cleanup

Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Superagent found 2 security concern(s).

.and_then(|name| name.to_str())
.unwrap_or("recording");

for entry in WalkDir::new(root).into_iter().filter_map(Result::ok) {

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: WalkDir follows symlinks, allowing arbitrary files outside recordings to be bundled

WalkDir follows symlinks by default during bundle creation, allowing files outside the recording directory to be included in the uploaded zip.

Call .follow_links(false) on WalkDir instances to prevent symlink traversal during profiling.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="apps/desktop/src-tauri/src/session_profile.rs">
<violation number="1" location="apps/desktop/src-tauri/src/session_profile.rs:394">
<priority>P2</priority>
<title>WalkDir follows symlinks, allowing arbitrary files outside recordings to be bundled</title>
<evidence>The build_profile_zip function uses WalkDir::new(root) without disabling symlink following. By default WalkDir follows symlinks, so a symlink placed inside a recording directory pointing to sensitive files outside the directory (e.g., /etc/passwd) would be opened and bundled into the zip, then uploaded to S3.</evidence>
<recommendation>Set .follow_links(false) on WalkDir instances in build_profile_zip (and directory_size for consistency) to prevent traversal of symlinks when discovering recording contents.</recommendation>
</violation>
</file>


let mut done: u64 = 0;

let file = std::fs::File::create(output_path)

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: Session profile temp bundle may be created with world-readable permissions

build_profile_zip creates the bundle in the system temp directory with default filesystem permissions, which may allow other local users to read sensitive user data during bundling.

Restrict the temp file permissions to owner-only immediately after creation, or use a secure temp directory.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="apps/desktop/src-tauri/src/session_profile.rs">
<violation number="1" location="apps/desktop/src-tauri/src/session_profile.rs:360">
<priority>P2</priority>
<title>Session profile temp bundle may be created with world-readable permissions</title>
<evidence>build_profile_zip calls std::fs::File::create(output_path) where output_path is a temp directory path. On Unix systems with a typical umask of 022, this creates the file with 644 permissions, making it world-readable. The bundle contains diagnostics, logs, and potentially recording data that should remain private to the user.</evidence>
<recommendation>Set restrictive file permissions (e.g., 0o600) on the bundle file immediately after creation. On Unix, use std::os::unix::fs::PermissionsExt, and consider applying the same for intermediate files.</recommendation>
</violation>
</file>

@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jun 21, 2026
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai please re-review the pr

Comment on lines +596 to +601
note ? `**Note:** ${note}` : null,
recordingLines.length > 0 ? "" : null,
recordingLines.length > 0 ? "**Recordings:**" : null,
...recordingLines,
"",
`**Download (expires in 7 days):** ${downloadUrl}`,

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.

P1 Download URL disappears from Discord when note is long. note appears before the download link in the message array. A note longer than ~1400 characters — well within the 4000-char schema and UI limit — pushes the download URL past Discord's 2000-character hard cut-off. Since the presigned URL is the only way for the team to retrieve the bundle (it is never displayed in the desktop UI), any profile submitted with a moderately detailed note becomes inaccessible. Moving the **Download …** line to appear before the note block guarantees it is always included.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/api/desktop/[...route]/root.ts
Line: 596-601

Comment:
**Download URL disappears from Discord when note is long.** `note` appears before the download link in the message array. A note longer than ~1400 characters — well within the 4000-char schema and UI limit — pushes the download URL past Discord's 2000-character hard cut-off. Since the presigned URL is the only way for the team to retrieve the bundle (it is never displayed in the desktop UI), any profile submitted with a moderately detailed note becomes inaccessible. Moving the `**Download …**` line to appear before the note block guarantees it is always included.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:flagged PR flagged for review by security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants