A retry after a failed attachment or a failed CompleteJob created another task for the same report. The task id is now stored as soon as the task is created, a retry with a stored id only re-attempts the attachment, a sent submission with a task id is skipped, and a CompleteJob failure is logged instead of returned.
11 KiB
golem15.feedback
The shared feedback-widget plugin for SummerCMS applications: an embeddable widget's public config and submit API behind a widget key and an Origin allow-list, a per-user "hide the widget" preference, the widget settings, and a job that files every submission as a G15Office task.
import _ "git.golem15.com/golem15/sm-feedback-plugin"
Module git.golem15.com/golem15/sm-feedback-plugin, package feedback, plugin ID golem15.feedback.
Overview
The plugin is a compiled SummerCMS plugin ported from the WinterCMS Golem15.Feedback plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go. It requires golem15.user.
The widget routes are public. The widget key in the URL is a public identifier (it ships in the embed snippet), not a secret: the real gate is the Origin check against the operator's allowed-origins list, backed by per-IP rate limits.
The plugin ID, the table names, the config keys and the settings code are a contract shared by every application that mounts the plugin: change them only in a backwards-compatible way.
Features
- Widget config.
GET /_feedback/api/v1/{key}/configanswers the widget's position, whether visitors may hide it, its colors and its labels (?lang=enselects the English labels, anything else the Polish ones). A disabled widget, an empty key or a key that differs (compared in constant time) is 404{"error":true,"message":"Not found"}. - Origin gate. A request without an
Originheader passes. AnOriginpasses only when its URL host, lower-cased, is on the allowed-origins list (one entry per line; a line is reduced to its URL host when it has one). An empty list refuses everyOriginwith 403{"error":true,"message":"Origin not allowed"}, so a widget that was never configured cannot be embedded anywhere. - Submissions.
POST /_feedback/api/v1/{key}/submit(multipart, from the widget) passes the same key and Origin gate, then validates the report as the WinterCMS plugin does:message(required, up to 5000 characters),type(bug,featureorother),email(optional, up to 255),page_url(required, up to 2000),user_agent(up to 500),console_log(up to 20000) andscreenshot(an optional JPEG, PNG, GIF or WebP of up to 10240 KB). A failure is 422{"error":true,"message":"Validation failed","details":{...}}. A screenshot whose bytes do not decode as one of those images (classes.IsAllowedImage) is 422 withdetails.screenshot = ["The file is not a valid image."]. The submission (statuspending), its public screenshot (attachOnescreenshot) and its G15Office job are written in one transaction, and the answer is 202{"success":true}: the visitor never waits on G15Office. - Hiding the widget.
PUT /_feedback/api/v1/me/hidden(behind golem15.user'sjwt.auth) stores the signed-in user's preference:hiddenmust be a Laravel boolean (true,false,1,0,"1","0"), else 422{"error":"Validation failed","errors":{...}}. The answer is{"hidden":bool}. Every golem15.user payload (fetch, login, update) carriesfeedback_widget_hiddenthrough aGetApiArrayEventlistener: the stored value, false without a row. - CORS. The widget's preflights are answered by the framework's path-scoped CORS layer, so the plugin registers no
OPTIONSroute. - Rate limits. The named buckets
feedback-config(60 a minute) andfeedback-submit(10 a minute) count per client IP behind the configured trusted proxies. - Settings.
models.Settingsis the singleton row ofgolem15_feedback_settings.models.Instancecreates it on first access with the WinterCMS defaults and a new widget key (wk_plus 32 random letters and digits). Operators edit it under Settings › Feedback Widget (codefeedback, permissiongolem15.feedback.manage_settings): the WinterCMS form with cabana's field types, the colors as text fields and the widget key read-only. A row saved without a key gets a new one. - G15Office tasks. Every submission queues a
classes.SyncG15OfficeArgsjob (kindgolem15.feedback.sync_g15office, queuefeedback, 3 attempts, asummer_jobsrow labelled with the WinterCMS job class).classes.SyncG15Officefiles it withclasses.G15OfficeClient:POST <base>/_support/api/v1/projects/<project>/taskswith the title (Feedback:and the message collapsed and cut to 80 columns), a Markdown description (the message, the page, the reporter email and user agent, the console log cut to 4000 columns), the task type (bug→ Bug,feature→ Feature,other→ Task), the configured task status and priority; thenPOST <base>/_support/api/v1/tasks/<id>/attachmentswith the screenshot as the multipart fieldfile. Requests carryAuthorization: Bearer <token>andAccept: application/jsonand go through afetchguard.Clientlimited to the base URL's host (https only). Success marks the submissionsentwith the task id; a failure marks itfailedwith the error and the job is retried. The task id is stored as soon as the task is created, so a retry re-attempts only the attachment, and a submission alreadysentwith a task id is never filed again. Without a base URL, token and project nothing is sent: the job fails with the WinterCMS "not configured" message. - Submissions list. Admin › Feedback › Submissions (controller
golem15.feedback.submissions) is a read-only list: type, status, reporter email, page, message, task id and date, all as plain text. - Widget script.
GET /plugins/golem15/feedback/assets/js/embed.jsserves the WinterCMS plugin'sembed.jsbyte for byte (application/javascript, with an ETag), at the path embedding pages already use. - Settings import.
feedback:import-settingscopies a WinterCMS installation's widget settings, the widget key included, into the singleton once.
Usage
Mount the plugin in an application as a git submodule at plugins/golem15/feedback:
git submodule add git@git.golem15.com:golem15/sm-feedback-plugin.git plugins/golem15/feedback
Add it to the application's go.work and go.mod:
use ./plugins/golem15/feedback
require git.golem15.com/golem15/sm-feedback-plugin v0.0.0
replace git.golem15.com/golem15/sm-feedback-plugin => ./plugins/golem15/feedback
List it in summer.yaml after golem15.user and run summer build:
module: git.golem15.com/golem15/acme
binary: acme
plugins:
- id: golem15.user
module: git.golem15.com/golem15/sm-user-plugin
- id: golem15.feedback
module: git.golem15.com/golem15/sm-feedback-plugin
The application's http.cors.paths must list _feedback/api/*: the widget calls the API from other origins. A page embeds the widget with its key:
<script src="https://backend.example.com/plugins/golem15/feedback/assets/js/embed.js" data-key="wk_..." data-lang="en" defer></script>
Set the G15Office connection with the three SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__* variables (see Configuration) and run a queue worker: serve runs one in-process when queue.work_in_serve is on, queue:work in its own process. After moving an installation from WinterCMS, run feedback:import-settings once so embed snippets keep their widget key.
The plugin's own go.mod replaces the framework with ../../../../summercms.go and the user plugin with ../user, so it expects the framework checkout next to the application checkout and the user plugin mounted beside it.
API reference
feedback:Plugin, thegolem15.feedbackplugin (config, lang, migrations, models, routes, buckets, jobs, commands, admin controllers, permissions, navigation, settings);BucketConfig,BucketSubmit,EmbedJSPath,EmbedJS,SettingsCode.controllers/api:Config,Submit,SubmitRules,MeHidden,MeHiddenRules,KeyMatches,OriginAllowed.classes:IsAllowedImage,SniffImageMIME,AllowedImageMIMEs;Screenshot,StoreSubmission,ScreenshotOf; the job contractSyncG15OfficeArgs,SyncG15OfficeKind,SyncG15OfficeQueue,SyncG15OfficeLabel,SyncG15OfficeAttempts; the jobSyncG15Office,SyncDeps,TaskTitle,TaskDescription,StrLimit,StrWidth; the clientG15OfficeClient,NewG15OfficeClient,NewG15OfficeClientWith,TaskFields,ErrNotConfigured,G15OfficeTimeout,ConfigBaseURL,ConfigToken,ConfigProject.controllers:SubmissionsAdminController,AdminControllers,PermissionManageSettings.console:ImportSettings,Import,IsNothingToImport,ErrSettingsCustomised.models:Settings(tablegolem15_feedback_settings),SettingsID,SettingsCode,DefaultSettings,Instance,NewWidgetKey,Labels,URLHost,ASCIILower;Submission(tablegolem15_feedback_submissions),StatusPending,StatusSent,StatusFailed,TypeBug,TypeFeature,TypeOther,ScreenshotField,G15OfficeTypeMap;UserPreference(tablegolem15_feedback_user_preferences),IsWidgetHidden,SetWidgetHidden;Register,All.updates: the migrations (202610030201_create_feedback_submissions,202610030202_create_feedback_user_preferences,202610030203_create_feedback_settings),Register,All.
Configuration
Defaults live in config/config.yaml and are merged under golem15.feedback.*.
| Key | Default | Meaning |
|---|---|---|
golem15.feedback.g15_office.base_url |
empty | The G15Office base URL submissions are filed under (https; requests may reach only its host). |
golem15.feedback.g15_office.token |
empty | The G15Office API token. Never commit it. |
golem15.feedback.g15_office.project |
empty | The G15Office project the tasks are created in. |
Override them with SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL, SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__TOKEN and SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__PROJECT.
CLI commands
| Command | Purpose |
|---|---|
feedback:import-settings [--force] |
Copy the WinterCMS system_settings row golem15_feedback_settings (widget key, origins, colors, labels, G15Office status and priority) into golem15_feedback_settings. It refuses settings that were already changed unless --force is given, and reports a missing system_settings table or row without failing. |
Dependencies
- The SummerCMS framework,
git.golem15.com/golem15/summercms(modulesbackpack,bonfire,bouncer,cabana,compass,conga,fetchguard,lagoon,pact,party,phrasebook,surf,wire;tidein tests). - gocloud.dev blob storage for the screenshots, through the application's uploads bucket.
- The user plugin,
git.golem15.com/golem15/sm-user-plugin. - GORM and gormigrate, as pinned in
go.mod. - testcontainers-go for the Postgres-backed tests.
Testing
go test ./... # starts Postgres with testcontainers (needs Docker)
classes/testdata/g15office-task.upstream.yaml is the G15Office exchange the WinterCMS job made, recorded through the framework's upstream recording proxy; TestSyncG15Office replays it offline and fails on any difference in the requests (URL, headers, the task JSON, the screenshot part).