Skip to content

feat(feedback): route in-app feedback to Slack via Cloudflare Worker relay#2745

Open
rabanspiegel wants to merge 6 commits into
mainfrom
emdash/in-app-feedback-7r8pr
Open

feat(feedback): route in-app feedback to Slack via Cloudflare Worker relay#2745
rabanspiegel wants to merge 6 commits into
mainfrom
emdash/in-app-feedback-7r8pr

Conversation

@rabanspiegel

Copy link
Copy Markdown
Contributor

What

In-app feedback now goes to the team Slack #feedback channel instead of a Discord webhook. Feedback text, image attachments, and the opt-in diagnostic log all carry over.

How

  • services/feedback-relay/ — a Cloudflare Worker that receives feedback (multipart/form-data: a content field plus file0…N) and posts it to Slack. It holds the Slack bot token as a Worker secret, so no token ships in the app. Files use Slack's external upload flow; text-only feedback uses chat.postMessage. An optional shared-secret header (x-emdash-feedback-secret) lets the Worker reject traffic that doesn't come from the app.
  • feedback.submit RPC — the renderer sends feedback to the main process, which POSTs to the relay. This keeps the relay URL and shared secret out of the renderer bundle.
  • Build config — the app reads VITE_FEEDBACK_RELAY_URL and VITE_FEEDBACK_RELAY_SECRET at build time. The setup-build action writes them into .env.production for release builds (same mechanism as the PostHog vars); the secret comes from the FEEDBACK_RELAY_SECRET GitHub secret.
  • Removed the hardcoded Discord webhook URL and its client-side POST. buildFeedbackContent moved to its own module so it stays unit-testable without a window.

Why

The Discord webhook URL shipped in the client bundle. Slack gives the team a more visible feedback channel, and issues can be created from Slack via the Linear integration. The Worker keeps the Slack credential server-side.

Notes

  • The Worker is deployed and its secrets are set. The old Discord webhook has been deleted.
  • services/feedback-relay/ is deployed independently and is not part of the pnpm workspace.
  • Verified: relay against live Slack (text, file upload, wrong-secret rejection), production build with both vars baked into the packaged asar, typecheck/lint/format/tests.
  • The .env.production CI path itself has not been run end-to-end; it will be confirmed in canary. If the FEEDBACK_RELAY_SECRET GitHub secret is missing or wrong, the relay returns 401 — watch wrangler tail when testing.

…relay

Replace the Discord webhook with a Cloudflare Worker that posts feedback to
the team Slack #feedback channel, keeping the Slack bot token out of the
shipped app.

- Add services/feedback-relay: a Worker that receives feedback (text plus
  image and diagnostic-log attachments) and forwards it to Slack. It holds
  the bot token as a secret and enforces an optional shared-secret header.
- Add a feedback.submit RPC. The renderer sends feedback to the main process,
  which POSTs to the relay, so the relay URL and shared secret stay out of the
  renderer bundle.
- Read VITE_FEEDBACK_RELAY_URL and VITE_FEEDBACK_RELAY_SECRET at build time;
  wire them into release builds via the setup-build action.
- Remove the hardcoded Discord webhook and its client-side POST.
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the hardcoded Discord webhook (which shipped in the renderer bundle) with a Cloudflare Worker relay that holds the Slack bot token server-side. Feedback submission is now routed through the main process via a new feedback.submit RPC, keeping the relay URL and shared secret out of the renderer.

  • New services/feedback-relay/ (deployed separately): a Cloudflare Worker that receives multipart/form-data and posts content + file attachments to Slack using the external upload API.
  • New feedback controller/service in the main process: reads VITE_FEEDBACK_RELAY_URL and VITE_FEEDBACK_RELAY_SECRET from build-time env, constructs a FormData POST, and sends Authorization: Bearer <secret> for lightweight relay authentication.
  • CI pipeline updated to bake VITE_FEEDBACK_RELAY_URL (with a hardcoded default pointing at the deployed Worker) and VITE_FEEDBACK_RELAY_SECRET (from GitHub Secrets) into .env.production at build time, following the same pattern already used for PostHog vars.

Confidence Score: 5/5

The change is safe to merge. The relay architecture correctly isolates the Slack bot token server-side, IPC transport of ArrayBuffers is handled correctly by Electron's Structured Clone, and the CI secret wiring mirrors the existing PostHog pattern.

All changed paths are well-guarded: the main-process service throws on a missing relay URL, the controller wraps every error into a typed response, and the renderer validates inputs before touching IPC. The only outstanding note (from a previous review thread) is the Worker-side timing-safe comparison, which is in the separately-deployed service and not part of this diff.

No files require special attention.

Important Files Changed

Filename Overview
apps/emdash-desktop/src/main/core/feedback/service.ts New service that constructs multipart/form-data and POSTs to the relay; guards against missing relay URL, attaches bearer secret when configured, and logs errors on non-OK responses.
apps/emdash-desktop/src/main/core/feedback/controller.ts Thin RPC controller wrapping the service; catches errors and returns a discriminated union so the renderer never receives unhandled rejections through IPC.
apps/emdash-desktop/src/renderer/lib/components/feedback-modal/use-feedback-submit.ts Replaces direct Discord webhook fetch with an RPC call; file-size and count validation unchanged, ArrayBuffer serialization across IPC is valid via Electron's Structured Clone.
.github/actions/setup-build/action.yml Adds feedback-relay-url (with hardcoded Worker URL as default) and feedback-relay-secret inputs; writes both into .env.production using the existing PostHog pattern.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant Renderer
    participant MainProcess
    participant CloudflareWorker
    participant SlackAPI

    User->>Renderer: Submit feedback form
    Renderer->>Renderer: "Validate inputs & build content"
    Renderer->>Renderer: Read files as ArrayBuffer
    Renderer->>MainProcess: "rpc.feedback.submit({content, files})"
    Note over Renderer,MainProcess: Electron IPC (Structured Clone)
    MainProcess->>MainProcess: "Read VITE_FEEDBACK_RELAY_URL & VITE_FEEDBACK_RELAY_SECRET from env"
    MainProcess->>CloudflareWorker: POST multipart/form-data Authorization: Bearer secret
    alt Has attachments
        CloudflareWorker->>SlackAPI: files.getUploadURLExternal
        CloudflareWorker->>SlackAPI: POST file bytes to upload URL
        CloudflareWorker->>SlackAPI: files.completeUploadExternal
    else Text only
        CloudflareWorker->>SlackAPI: chat.postMessage
    end
    CloudflareWorker-->>MainProcess: 200 OK
    MainProcess-->>Renderer: "{success: true}"
    Renderer-->>User: Success toast
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant Renderer
    participant MainProcess
    participant CloudflareWorker
    participant SlackAPI

    User->>Renderer: Submit feedback form
    Renderer->>Renderer: "Validate inputs & build content"
    Renderer->>Renderer: Read files as ArrayBuffer
    Renderer->>MainProcess: "rpc.feedback.submit({content, files})"
    Note over Renderer,MainProcess: Electron IPC (Structured Clone)
    MainProcess->>MainProcess: "Read VITE_FEEDBACK_RELAY_URL & VITE_FEEDBACK_RELAY_SECRET from env"
    MainProcess->>CloudflareWorker: POST multipart/form-data Authorization: Bearer secret
    alt Has attachments
        CloudflareWorker->>SlackAPI: files.getUploadURLExternal
        CloudflareWorker->>SlackAPI: POST file bytes to upload URL
        CloudflareWorker->>SlackAPI: files.completeUploadExternal
    else Text only
        CloudflareWorker->>SlackAPI: chat.postMessage
    end
    CloudflareWorker-->>MainProcess: 200 OK
    MainProcess-->>Renderer: "{success: true}"
    Renderer-->>User: Success toast
Loading

Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread services/feedback-relay/src/index.ts Outdated
Comment thread services/feedback-relay/src/index.ts Outdated

const uploadUrl = String(urlData.upload_url);
const fileId = String(urlData.file_id);
const putResponse = await fetch(uploadUrl, { method: 'POST', body: file });

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 Misleading variable name for the upload step

The variable is called putResponse but the request uses method: 'POST'. Slack's external-upload URL does in fact expect a POST, so this is functionally correct, but the name implies PUT and will confuse anyone cross-checking against older Slack documentation that described a PUT step. Consider renaming to uploadResponse.

Prompt To Fix With AI
This is a comment left during a code review.
Path: services/feedback-relay/src/index.ts
Line: 110

Comment:
**Misleading variable name for the upload step**

The variable is called `putResponse` but the request uses `method: 'POST'`. Slack's external-upload URL does in fact expect a POST, so this is functionally correct, but the name implies PUT and will confuse anyone cross-checking against older Slack documentation that described a PUT step. Consider renaming to `uploadResponse`.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

rabanspiegel and others added 2 commits July 1, 2026 17:07
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
The Slack external-upload request uses POST, so putResponse was misleading.
@rabanspiegel rabanspiegel requested a review from Davidknp July 2, 2026 20:27
The Cloudflare Worker now lives in generalaction/emdash-server. This repo keeps
only the client integration (feedback.submit RPC + build vars).
…k-7r8pr

# Conflicts:
#	apps/emdash-desktop/src/renderer/lib/components/feedback-modal/use-feedback-submit.ts
@rabanspiegel

Copy link
Copy Markdown
Contributor Author

@greptileai

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant