Skip to content

Added a screenshot upload feature in the feedback launcher#3508

Merged
Baalmart merged 20 commits into
airqo-platform:stagingfrom
Rexalguy:feat/FeedbackScreenshot
May 21, 2026
Merged

Added a screenshot upload feature in the feedback launcher#3508
Baalmart merged 20 commits into
airqo-platform:stagingfrom
Rexalguy:feat/FeedbackScreenshot

Conversation

@Rexalguy

@Rexalguy Rexalguy commented May 19, 2026

Copy link
Copy Markdown
Contributor

Screenshot Upload for Feedback Submission

Closes #3456

Summary

Adds screenshot upload functionality to the feedback submission system in the Vertex application, enabling users to attach visual context when reporting issues or suggesting improvements.


Changes

Screenshot Capture and Upload

  • File input with drag-and-drop UI for screenshot selection
  • Cloudinary integration for image hosting
  • Client-side validation for file type and size
  • Maximum file size: 2MB
  • Supported formats: JPG, PNG, GIF, WEBP

Frontend (feedback-launcher.tsx)

  • New state management for screenshot handling (screenshotFile, isUploadingScreenshot)
  • File validation function with clear error messages
  • handleScreenshotChange() and handleRemoveScreenshot() handlers
  • Screenshot upload to Cloudinary within feedback submission flow
  • Graceful fallback if upload fails — feedback still submits without screenshot
  • Updated button states to reflect upload progress
  • Responsive UI with dark mode support

API (feedback.ts)

  • Added screenshot_url?: string field to SubmitFeedbackRequest interface

Authentication (options.ts)

  • Improved auth secret fallback logic for development environments
  • Added warning log when using dev fallback secret

Known Issue

Backend Cloudinary Configuration Mismatch

  • The screenshot URL is successfully generated and uploaded to Cloudinary
  • The backend is not configured with the same Cloudinary account credentials, causing it to reject or fail to validate the screenshot URL during feedback submission
  • Workaround: Feedback submission completes successfully even if the screenshot URL fails (non-blocking error)

Resolution required: The backend team must configure their Cloudinary account with matching credentials before this feature can be fully utilised in production.


Maturity Checklist

  • Tested locally
  • Code considered complete
  • Ready for production : blocked by backend Cloudinary configuration
  • PR title states what changed and includes related issue number
  • Issue number included in "Closes" section above
  • Corresponding documentation updated
  • Unit and/or e2e tests written

Manual Testing

Setup

  1. Ensure the Vertex frontend is running locally
  2. Navigate to any page in the application
  3. Trigger the feedback dialog via the feedback button or event

Test Cases

1. Screenshot Upload Flow

  • Click "Report an issue" or "Suggest an idea"
  • Click the screenshot input area
  • Select a valid image file (JPG, PNG, GIF, or WEBP)
  • Verify the filename appears below the upload area
  • Click "Remove" to delete the selected screenshot
  • Re-select an image to proceed

2. Validation

  • Upload a file larger than 2MB : expected error: "File is too large"
  • Upload a non-image file (e.g. .txt) : expected error: "Unsupported file format"
  • Upload an invalid image format : expected: error shown and file rejected

3. Submission with Screenshot

  • Fill out all required fields and select a valid screenshot
  • Click "Send"
  • Verify the button shows "Uploading..." then "Sending..."
  • Check the browser console for a successful Cloudinary upload
  • Verify the toast shows "Feedback sent successfully"

4. Submission without Screenshot

  • Fill out the feedback form without selecting a screenshot
  • Click "Send"
  • Verify the submission completes normally

5. Upload Failure : Graceful Handling

  • Simulate a Cloudinary upload failure (e.g. via network throttling)
  • Verify the toast shows: "Screenshot upload failed - We could not attach the screenshot..."
  • Verify feedback still submits successfully
  • Confirm screenshot_url is undefined in the backend payload

Screenshot
image

Summary by CodeRabbit

  • New Features

    • Built-in screenshot capture, annotation, full-screen preview, thumbnail, remove/re-capture, and attach flow in feedback.
    • Feedback now sends an optional screenshot URL when available.
  • Improvements

    • Shows upload vs send progress; preview closable with Escape; displays captured filename/size.
    • Stronger validation (issue action required for issues) and clearer submit error messaging; submission proceeds if upload fails.
  • Documentation

    • Changelog updated with screenshot capability.
  • Chores

    • Example env file updated with Cloudinary placeholders.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds client screenshot capture and annotation to the Feedback launcher with optional Cloudinary upload (sends resulting screenshot_url in SubmitFeedbackRequest); adds server upload route and Cloudinary env getters; logs a non-production warning when NEXTAUTH secret is empty.

Changes

Feedback Screenshot Attachment

Layer / File(s) Summary
API contract & changelog
src/vertex/core/apis/feedback.ts, src/vertex/app/changelog.md
Adds optional screenshot_url to SubmitFeedbackRequest and prepends Version 1.23.46 changelog entry documenting the screenshot feature.
Component imports, types & browser helpers
src/vertex/components/features/feedback/feedback-launcher.tsx
Updates imports and types (Rect), adds highlight rendering constants, and refactors browser label extraction used by the screenshot flow.
Annotator core, flattening & capture implementation
src/vertex/components/features/feedback/feedback-launcher.tsx
Adds flattenAnnotations, ScreenshotAnnotator full-screen canvas editor and captureScreenshot using getDisplayMedia + ImageCapture fallback; manages screenshot state, preview modal, and reset-on-close behavior.
UI step-2, validation, submit wiring
src/vertex/components/features/feedback/feedback-launcher.tsx
Extends Step 2 UI with capture button, thumbnail/preview, filename/size, remove action; requires issueAction for mainCategory === 'issue'; conditionally uploads annotated File to Cloudinary, includes screenshot_url in submit payload, and handles upload failures as warnings while proceeding.
Client Cloudinary API adjustments
src/vertex/core/apis/cloudinary.ts
Switches to getCloudinaryName()/getCloudinaryPreset(), reduces MAX_FILE_SIZE to 2MB, posts file FormData to internal /api/cloudinary/upload, checks response.ok, standardizes errors, and updates cloudinaryImageUpload to accept FormData.
Server upload route & env getters
src/vertex/app/api/cloudinary/upload/route.ts, src/vertex/lib/envConstants.ts, src/vertex/.env.example
Adds POST upload route that validates and signs/uploads to Cloudinary (15s timeout) and returns secure_url/public_id; adds Cloudinary env getter functions, exposes cloudinaryName/cloudinaryPreset in getEnvConfig, updates validateEnvironment to require Cloudinary vars, and updates .env.example.

Auth Secret Initialization with Fallback

Layer / File(s) Summary
Secret missing non-production warning & fallback
src/vertex/app/api/auth/[...nextauth]/options.ts
Adds a non-production log for empty NEXTAUTH secret between existing production-critical log and dynamic host warning; exported NextAuth options remains unchanged.

Sequence Diagram

sequenceDiagram
  participant User
  participant FeedbackLauncher
  participant CloudinaryUploadAPI
  participant Cloudinary
  participant FeedbackAPI
  User->>FeedbackLauncher: Capture & annotate screenshot
  FeedbackLauncher->>CloudinaryUploadAPI: POST file (FormData)
  CloudinaryUploadAPI->>Cloudinary: Signed upload request (server-side)
  Cloudinary-->>CloudinaryUploadAPI: secure_url
  CloudinaryUploadAPI-->>FeedbackLauncher: secure_url
  FeedbackLauncher->>FeedbackAPI: POST feedback payload (includes screenshot_url)
  FeedbackAPI-->>FeedbackLauncher: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

ready for review

Suggested reviewers

  • Codebmk
  • Baalmart

Poem

📸 A tiny capture, a rectangle bright,
Users mark edges to show what’s not right.
The annotator flattens strokes into light,
Cloudinary carries the image in flight,
Secrets whispered softly for dev’s quiet night.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes include auth secret logging, environment variable management, and Cloudinary API infrastructure beyond the scope of adding screenshot upload to the feedback launcher. The auth secret fallback log and Cloudinary infrastructure (envConstants, route handler, cloudinary.ts refactor) are necessary supporting work for the screenshot feature; review whether auth changes belong in this PR or should be separated.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: screenshot upload capability in the feedback launcher component.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #3456: file upload UI in feedback launcher, 2MB/format validation, Cloudinary integration with optional screenshot, and graceful fallback on upload failure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

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.

Pull request overview

Adds optional screenshot attachment support to Vertex’s feedback launcher so users can include visual context when submitting issues/ideas, alongside a small NextAuth secret fallback improvement.

Changes:

  • Adds screenshot file selection UI + client-side validation and attempts Cloudinary upload before submitting feedback.
  • Extends feedback submission request type to include optional screenshot_url.
  • Tweaks NextAuth secret resolution for local development and logs a warning when using a dev fallback secret.

Reviewed changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/vertex/components/features/feedback/feedback-launcher.tsx Adds screenshot selection/validation state and wires Cloudinary upload into the feedback submission flow.
src/vertex/core/apis/feedback.ts Adds optional screenshot_url to SubmitFeedbackRequest.
src/vertex/app/api/auth/[...nextauth]/options.ts Improves auth secret fallback behavior and adds a dev warning log.
src/vertex/package-lock.json Updates resolved dependency versions in the Vertex app lockfile.
package-lock.json Introduces a new root lockfile (currently appears to be a stub).
Files not reviewed (1)
  • src/vertex/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +207 to +213
try {
const uploadedScreenshot = await uploadToCloudinary(screenshotFile, {
folder: 'feedback',
tags: ['vertex', 'feedback'],
});

screenshot_url = uploadedScreenshot.secure_url;
Comment on lines +301 to +316
<div className="rounded-xl border border-dashed border-gray-300 bg-white p-4 transition-colors hover:border-primary/60 dark:border-gray-700 dark:bg-gray-800">
<label
htmlFor="screenshot"
className="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
Capture screenshot
</label>

<input
ref={screenshotInputRef}
id="screenshot"
type="file"
accept={ALLOWED_FEEDBACK_SCREENSHOT_MIME_TYPES.join(',')}
onChange={handleScreenshotChange}
className="block w-full cursor-pointer rounded-lg border border-gray-300 bg-white text-sm text-gray-700 shadow-sm file:mr-4 file:cursor-pointer file:rounded-md file:border-0 file:bg-primary file:px-4 file:py-2 file:text-sm file:font-medium file:text-white hover:file:bg-primary/90 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200"
/>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/vertex/components/features/feedback/feedback-launcher.tsx (2)

500-510: 💤 Low value

Silent failure if toBlob returns null.

If canvas.toBlob returns null (rare, but possible under memory pressure), the user receives no feedback and screenshotFile remains unset. Consider adding an error toast for this edge case.

🔧 Handle null blob case
           canvas.toBlob(
             (blob) => {
               if (blob) {
                 setScreenshotDataUrl(dataUrl);
                 setScreenshotFile(new File([blob], 'screenshot.jpg', { type: 'image/jpeg' }));
                 toast.success('Screenshot captured successfully');
+              } else {
+                toast.error('Failed to process screenshot');
               }
             },
             'image/jpeg',
             0.9,
           );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/feedback/feedback-launcher.tsx` around lines
500 - 510, The canvas.toBlob callback currently ignores the null-blob edge case,
causing silent failure; update the callback used in feedback-launcher.tsx so
that if the blob is null you call toast.error with a helpful message (e.g.,
"Failed to capture screenshot"), clear or leave setScreenshotFile unset
explicitly (or set it to null) and avoid calling setScreenshotDataUrl; keep the
existing success branch that calls setScreenshotDataUrl and setScreenshotFile
when blob is present.

170-177: 💤 Low value

Consider adding cleanup to prevent state updates on unmounted component.

If the component unmounts before the image finishes loading, setImgLoaded(true) will fire on an unmounted component. While React 18 is more lenient about this, it's still good practice to add a cleanup flag.

🔧 Suggested cleanup pattern
   useEffect(() => {
+    let cancelled = false;
     const img = new Image();
     img.onload = () => {
+      if (cancelled) return;
       imgRef.current = img;
       setImgLoaded(true);
     };
     img.src = baseDataUrl;
+    return () => { cancelled = true; };
   }, [baseDataUrl]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/feedback/feedback-launcher.tsx` around lines
170 - 177, The useEffect that creates an Image and sets imgRef.current and calls
setImgLoaded(true) can update state after the component unmounts; modify the
effect (the useEffect that depends on baseDataUrl and uses imgRef and
setImgLoaded) to use a mounted/aborted flag (or remove the onload handler in
cleanup) so the img.onload handler checks the flag before calling setImgLoaded
and assigning imgRef.current, and ensure you clean up the Image.onload handler
in the return cleanup function to avoid memory leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vertex/components/features/feedback/feedback-launcher.tsx`:
- Around line 434-440: The await Promise in feedback-launcher.tsx that waits for
track.readyState === 'live' can hang indefinitely; modify the logic in that
block to add a timeout (e.g., 5–10s) so the promise always settles: if
track.readyState === 'live' resolve immediately, otherwise attach the 'unmute'
listener on track and also start a timer that rejects or resolves after the
timeout; ensure you remove the event listener and clear the timeout in both the
listener and timeout handlers to avoid leaks and race conditions—update the
Promise creation around track.readyState / track.addEventListener to include
this timeout and cleanup.
- Around line 838-867: Add a keyboard handler for the preview modal: in the same
component where previewOpen, setPreviewOpen, and screenshotDataUrl are defined,
add a useEffect that, when previewOpen is true, registers a global keydown
listener that closes the modal by calling setPreviewOpen(false) when event.key
=== 'Escape', and cleans up the listener when previewOpen becomes false or the
component unmounts; ensure the effect depends on previewOpen and setPreviewOpen
so the Escape key dismisses the preview the same as clicking the backdrop or the
close button (X).
- Around line 446-448: The code directly instantiates ImageCapture and calls
grabFrame without feature detection; update the capture block to first check for
ImageCapture support (e.g., typeof ImageCapture !== "undefined" ||
window.ImageCapture) and only then call new ImageCapture(track) and
imageCapture.grabFrame(); if ImageCapture is unavailable, implement a
canvas-based fallback that creates an offscreen or hidden canvas, draws the
current video frame via canvas.getContext('2d').drawImage(videoElement,
0,0,width,height) (or use an ImageBitmap if available), and export the image via
canvas.toBlob() or canvas.toDataURL(); in both paths ensure track.stop() is
still called and that errors are handled with a clearer message ("Image capture
not supported; used canvas fallback" or the specific error) and reference the
existing variables imageCapture, grabFrame, bitmap, and track to locate and
replace the block.

---

Nitpick comments:
In `@src/vertex/components/features/feedback/feedback-launcher.tsx`:
- Around line 500-510: The canvas.toBlob callback currently ignores the
null-blob edge case, causing silent failure; update the callback used in
feedback-launcher.tsx so that if the blob is null you call toast.error with a
helpful message (e.g., "Failed to capture screenshot"), clear or leave
setScreenshotFile unset explicitly (or set it to null) and avoid calling
setScreenshotDataUrl; keep the existing success branch that calls
setScreenshotDataUrl and setScreenshotFile when blob is present.
- Around line 170-177: The useEffect that creates an Image and sets
imgRef.current and calls setImgLoaded(true) can update state after the component
unmounts; modify the effect (the useEffect that depends on baseDataUrl and uses
imgRef and setImgLoaded) to use a mounted/aborted flag (or remove the onload
handler in cleanup) so the img.onload handler checks the flag before calling
setImgLoaded and assigning imgRef.current, and ensure you clean up the
Image.onload handler in the return cleanup function to avoid memory leaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6ec1b89-334c-4cc1-bc8e-af97b54f7968

📥 Commits

Reviewing files that changed from the base of the PR and between 668f824 and f88fb9e.

📒 Files selected for processing (2)
  • src/vertex/app/changelog.md
  • src/vertex/components/features/feedback/feedback-launcher.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/vertex/app/changelog.md

Comment thread src/vertex/components/features/feedback/feedback-launcher.tsx
Comment thread src/vertex/components/features/feedback/feedback-launcher.tsx Outdated
Comment thread src/vertex/components/features/feedback/feedback-launcher.tsx

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/vertex/components/features/feedback/feedback-launcher.tsx (1)

549-576: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce the 2 MB cap before storing the generated screenshot.

Line 551 and Line 580 both persist a freshly generated JPEG without checking its size first. Full-browser captures can easily exceed 2,097,152 bytes on HiDPI displays, so this pushes a required validation failure to upload time instead of warning immediately in the UI.

💡 Minimal guard
        canvas.toBlob(
          (blob) => {
            if (blob) {
+             if (blob.size > 2_097_152) {
+               showBanner({
+                 severity: 'warning',
+                 message: 'Screenshots must be 2 MB or smaller.',
+                 scoped: true,
+               });
+               return;
+             }
              setScreenshotDataUrl(dataUrl);
              setScreenshotFile(new File([blob], 'screenshot.jpg', { type: 'image/jpeg' }));
              showBanner({ severity: 'success', message: 'Screenshot captured successfully', scoped: true });
            }
          },
          'image/jpeg',
          0.9,
        );
        return;
      }

      const { dataUrl, file } = await flattenAnnotations(
        rawDataUrl,
        rects,
        rawDimensions.w,
        rawDimensions.h,
      );

+      if (file.size > 2_097_152) {
+        showBanner({
+          severity: 'warning',
+          message: 'Screenshots must be 2 MB or smaller.',
+          scoped: true,
+        });
+        return;
+      }
       setScreenshotDataUrl(dataUrl);
       setScreenshotFile(file);

Also applies to: 580-588

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/feedback/feedback-launcher.tsx` around lines
549 - 576, When converting the canvas to a JPEG and before calling
setScreenshotDataUrl/setScreenshotFile (inside the canvas.toBlob callback)
enforce a 2,097,152 byte size cap: check blob.size and if it exceeds the cap,
call showBanner with a clear error (e.g., "Screenshot exceeds 2MB") and do not
set the data URL or file; otherwise proceed to setScreenshotDataUrl(dataUrl) and
setScreenshotFile(new File(...)). Apply the same size check in the other
canvas.toBlob branch later in this file so both places that persist the
generated screenshot validate blob.size before storing or showing success.
src/vertex/core/apis/cloudinary.ts (1)

93-99: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Legacy uploader now swallows HTTP failures.

cloudinaryImageUpload no longer checks response.ok, so 4xx/5xx responses resolve as normal JSON and can propagate as false “success” to callers.

Suggested fix
 export const cloudinaryImageUpload = async (formData: FormData) => {
   const { cloudinaryUrl } = getCloudinaryConfig();
-  return await fetch(`${cloudinaryUrl}/image/upload`, {
+  const response = await fetch(`${cloudinaryUrl}/image/upload`, {
     method: 'POST',
     body: formData,
-  }).then((response) => response.json());
+  });
+  const result = await response.json();
+  if (!response.ok) {
+    throw new Error(result?.error?.message || 'Cloudinary upload failed');
+  }
+  return result;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/core/apis/cloudinary.ts` around lines 93 - 99,
cloudinaryImageUpload currently returns response.json() for any HTTP status, so
4xx/5xx results are treated as success; update cloudinaryImageUpload to await
fetch, check response.ok, and if false read the response body (e.g., await
response.text() or response.json()) and throw an error containing the HTTP
status and the response body/details, otherwise return the parsed JSON;
reference the cloudinaryImageUpload function and the response variable when
implementing this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vertex/app/api/cloudinary/upload/route.ts`:
- Around line 9-10: The code currently trusts the client-provided folder
variable (folder) when building the Cloudinary upload signature; stop using
client-supplied folders by either hardcoding folder = 'feedback' or validating
against a server-side whitelist before signing. Locate the upload route handler
where folder is read from formData (the folder variable and where signature is
generated) and replace uses of the untrusted folder with a constant ('feedback')
or a validated value derived from a server-side whitelist before creating the
signature and upload params (also update any other spots that read folder around
the tags handling and later signature generation).
- Around line 8-14: Validate the uploaded File server-side before signing or
returning upload data: check the "file" object's presence, its size (using
file.size) against the max allowed bytes (e.g., MAX_UPLOAD_BYTES constant) and
its MIME type (file.type) or extension against an allowlist (e.g.,
ALLOWED_IMAGE_TYPES), and return a 400 JSON error if any check fails; update the
code paths that create a signed upload (the code that reads const file =
formData.get('file') and the later signing/upload block) to perform the same
validation so direct callers cannot obtain a signed URL for
oversized/unsupported files.
- Around line 66-69: The Cloudinary upstream fetch (the call using
fetch(cloudinaryUrl, { method: 'POST', body: cloudinaryFormData })) can
hang—wrap it with an AbortController-based timeout: create an AbortController,
pass controller.signal to the fetch, schedule a setTimeout that calls
controller.abort() after a reasonable limit, and clear the timeout when fetch
completes; update the surrounding handler (in route.ts around the fetch call) to
catch the abort error and return a 504 or appropriate error response. Ensure you
reference cloudinaryUrl, cloudinaryFormData, the fetch call, and the created
AbortController so the timeout logic cleanly aborts the upstream request and
frees route workers.

---

Outside diff comments:
In `@src/vertex/components/features/feedback/feedback-launcher.tsx`:
- Around line 549-576: When converting the canvas to a JPEG and before calling
setScreenshotDataUrl/setScreenshotFile (inside the canvas.toBlob callback)
enforce a 2,097,152 byte size cap: check blob.size and if it exceeds the cap,
call showBanner with a clear error (e.g., "Screenshot exceeds 2MB") and do not
set the data URL or file; otherwise proceed to setScreenshotDataUrl(dataUrl) and
setScreenshotFile(new File(...)). Apply the same size check in the other
canvas.toBlob branch later in this file so both places that persist the
generated screenshot validate blob.size before storing or showing success.

In `@src/vertex/core/apis/cloudinary.ts`:
- Around line 93-99: cloudinaryImageUpload currently returns response.json() for
any HTTP status, so 4xx/5xx results are treated as success; update
cloudinaryImageUpload to await fetch, check response.ok, and if false read the
response body (e.g., await response.text() or response.json()) and throw an
error containing the HTTP status and the response body/details, otherwise return
the parsed JSON; reference the cloudinaryImageUpload function and the response
variable when implementing this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a624ca40-e384-4b49-8e0a-d989b38ba9d2

📥 Commits

Reviewing files that changed from the base of the PR and between f88fb9e and aecc100.

📒 Files selected for processing (6)
  • src/vertex/.env.example
  • src/vertex/app/api/auth/[...nextauth]/options.ts
  • src/vertex/app/api/cloudinary/upload/route.ts
  • src/vertex/components/features/feedback/feedback-launcher.tsx
  • src/vertex/core/apis/cloudinary.ts
  • src/vertex/lib/envConstants.ts
✅ Files skipped from review due to trivial changes (1)
  • src/vertex/app/api/auth/[...nextauth]/options.ts

Comment thread src/vertex/app/api/cloudinary/upload/route.ts
Comment thread src/vertex/app/api/cloudinary/upload/route.ts Outdated
Comment thread src/vertex/app/api/cloudinary/upload/route.ts

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/vertex/components/features/feedback/feedback-launcher.tsx (2)

580-591: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing success banner when annotations are applied.

When rects.length === 0, a success banner is shown (line 571). However, when annotations are flattened via flattenAnnotations, no success banner is displayed. This creates an inconsistent user experience.

🎨 Proposed fix
       const { dataUrl, file } = await flattenAnnotations(
         rawDataUrl,
         rects,
         rawDimensions.w,
         rawDimensions.h,
       );
 
       setScreenshotDataUrl(dataUrl);
       setScreenshotFile(file);
+      showBanner({ severity: 'success', message: 'Screenshot captured successfully', scoped: true });
     } catch {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/feedback/feedback-launcher.tsx` around lines
580 - 591, After successful flattening of annotations, add the same success
banner call that is shown when rects.length === 0 so users get consistent
feedback; specifically, after the await flattenAnnotations(...) and after
calling setScreenshotDataUrl(...) and setScreenshotFile(...), invoke
showBanner({ severity: 'success', message: 'Screenshot annotations applied',
scoped: true }) (or reuse the existing success message string) so that
flattenAnnotations, setScreenshotDataUrl, setScreenshotFile and showBanner are
updated together to display success when annotations were applied.

496-522: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Potential track leak if fallback errors before track.stop().

If an exception occurs in the video fallback path (e.g., at canvas.getContext or earlier), the track won't be stopped, potentially leaving the screen-share indicator active. Consider wrapping the capture logic in a try/finally or ensuring track.stop() is called in the outer catch block.

🛡️ Proposed fix
     } catch (error: unknown) {
+      // Ensure any lingering tracks are stopped
+      try {
+        const tracks = (error as any)?.stream?.getVideoTracks?.() ?? [];
+        tracks.forEach((t: MediaStreamTrack) => t.stop());
+      } catch { /* ignore */ }
       if (error instanceof Error && error.name === 'NotAllowedError') return; // user cancelled
       showBanner({ severity: 'error', message: 'Failed to capture screenshot', scoped: true });

Alternatively, store the track in a variable at the top of the try block and call track?.stop() in the catch/finally:

   const captureScreenshot = async () => {
     setIsCapturing(true);
     await new Promise<void>((resolve) => setTimeout(resolve, 50));
 
+    let track: MediaStreamTrack | null = null;
     try {
       const stream = await navigator.mediaDevices.getDisplayMedia({
         video: { displaySurface: 'browser' },
         audio: false,
       } as DisplayMediaStreamOptions);
 
-      const track = stream.getVideoTracks()[0];
+      track = stream.getVideoTracks()[0];
       // ... rest of capture logic
     } catch (error: unknown) {
+      track?.stop();
       if (error instanceof Error && error.name === 'NotAllowedError') return;
       showBanner({ severity: 'error', message: 'Failed to capture screenshot', scoped: true });
     } finally {
       setIsCapturing(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/feedback/feedback-launcher.tsx` around lines
496 - 522, The video fallback capture path can throw before calling
track.stop(), leaking the media track; modify the logic in feedback-launcher.tsx
so the MediaStreamTrack (track) is saved to a local variable before the capture
block and ensure track?.stop() is invoked in a finally block (or catch +
finally) around the video/canvas ops that produce dataUrl, captureWidth and
captureHeight; alternatively wrap the inner capture (video creation, await
onloadedmetadata, canvas.getContext/drawImage, canvas.toDataURL) in a
try/finally and call track?.stop() in the finally to guarantee the track is
always stopped even if getContext or drawImage fails.
🧹 Nitpick comments (2)
src/vertex/components/features/feedback/feedback-launcher.tsx (1)

204-211: 💤 Low value

Minor optimization opportunity in canvas dimension effect.

This effect runs whenever redraw changes (i.e., on every rect update), but canvas dimensions only need to be set once when the image loads. Consider splitting the dimension-setting logic from the redraw trigger, or guarding with a ref to track if dimensions were already set. Not a functional issue, just a slight inefficiency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/feedback/feedback-launcher.tsx` around lines
204 - 211, The effect currently tied to useEffect with dependencies [imgLoaded,
redraw] sets canvas dimensions every time redraw changes; separate the
responsibilities by moving dimension-setting into its own effect that only
depends on imgLoaded (or use a dimensionsSetRef guard) so
canvasRef/current.width and .height are set once when the image loads, while the
existing redraw effect continues to run on redraw changes; update references to
canvasRef, imgRef, imgLoaded, and redraw accordingly (or add a dimensionsSetRef
flag) to avoid resetting dimensions on every rect update.
src/vertex/core/apis/cloudinary.ts (1)

93-99: ⚡ Quick win

Align cloudinaryImageUpload error handling with uploadToCloudinary (src/vertex/core/apis/cloudinary.ts:93-99)

cloudinaryImageUpload currently returns response.json() without checking response.ok and without try/catch, so callers (if any external/back-compat consumers) may see raw Cloudinary error payloads or unhandled fetch rejections. There are no in-repo call sites found, but it should still mirror uploadToCloudinary’s response.ok handling and error normalization for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/core/apis/cloudinary.ts` around lines 93 - 99,
cloudinaryImageUpload currently returns response.json() without handling non-OK
responses or network errors; update cloudinaryImageUpload to mirror
uploadToCloudinary by wrapping the fetch in try/catch, await the response, check
response.ok and parse the body, and throw a normalized error (including
response.status and parsed error payload) when !response.ok so callers receive
the same error shape as uploadToCloudinary; reference function names
cloudinaryImageUpload and uploadToCloudinary to locate and align behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/vertex/components/features/feedback/feedback-launcher.tsx`:
- Around line 580-591: After successful flattening of annotations, add the same
success banner call that is shown when rects.length === 0 so users get
consistent feedback; specifically, after the await flattenAnnotations(...) and
after calling setScreenshotDataUrl(...) and setScreenshotFile(...), invoke
showBanner({ severity: 'success', message: 'Screenshot annotations applied',
scoped: true }) (or reuse the existing success message string) so that
flattenAnnotations, setScreenshotDataUrl, setScreenshotFile and showBanner are
updated together to display success when annotations were applied.
- Around line 496-522: The video fallback capture path can throw before calling
track.stop(), leaking the media track; modify the logic in feedback-launcher.tsx
so the MediaStreamTrack (track) is saved to a local variable before the capture
block and ensure track?.stop() is invoked in a finally block (or catch +
finally) around the video/canvas ops that produce dataUrl, captureWidth and
captureHeight; alternatively wrap the inner capture (video creation, await
onloadedmetadata, canvas.getContext/drawImage, canvas.toDataURL) in a
try/finally and call track?.stop() in the finally to guarantee the track is
always stopped even if getContext or drawImage fails.

---

Nitpick comments:
In `@src/vertex/components/features/feedback/feedback-launcher.tsx`:
- Around line 204-211: The effect currently tied to useEffect with dependencies
[imgLoaded, redraw] sets canvas dimensions every time redraw changes; separate
the responsibilities by moving dimension-setting into its own effect that only
depends on imgLoaded (or use a dimensionsSetRef guard) so
canvasRef/current.width and .height are set once when the image loads, while the
existing redraw effect continues to run on redraw changes; update references to
canvasRef, imgRef, imgLoaded, and redraw accordingly (or add a dimensionsSetRef
flag) to avoid resetting dimensions on every rect update.

In `@src/vertex/core/apis/cloudinary.ts`:
- Around line 93-99: cloudinaryImageUpload currently returns response.json()
without handling non-OK responses or network errors; update
cloudinaryImageUpload to mirror uploadToCloudinary by wrapping the fetch in
try/catch, await the response, check response.ok and parse the body, and throw a
normalized error (including response.status and parsed error payload) when
!response.ok so callers receive the same error shape as uploadToCloudinary;
reference function names cloudinaryImageUpload and uploadToCloudinary to locate
and align behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b216bcc4-df62-4677-be02-f03eb9bead98

📥 Commits

Reviewing files that changed from the base of the PR and between aecc100 and 01e4def.

📒 Files selected for processing (3)
  • src/vertex/app/api/cloudinary/upload/route.ts
  • src/vertex/components/features/feedback/feedback-launcher.tsx
  • src/vertex/core/apis/cloudinary.ts

@Codebmk Codebmk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Brilliant work @Rexalguy! I've added several enhancements to the UI.

Image Image Image Image Image Image

@Baalmart
Baalmart merged commit af1fb83 into airqo-platform:staging May 21, 2026
1 check passed
@Baalmart Baalmart mentioned this pull request May 21, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Vertex: Feature] Add Screenshot Support to Feedback Launcher

4 participants