WIP This is currently tailored to the use cases for Code Q, feel free to fork your own version of add PRs
A reusable Neos CMS package that adds a visual feedback widget to the rendered website. Visitors capture a screenshot of the current page, annotate it (freehand, rectangle, arrow, text, undo/redo, delete), give the report a title and description — and every submission creates exactly one task in a fixed Asana project, including the annotated screenshot as attachment, the page URL, the author and technical browser context. It replaces Marker.io for this use case.
- Screenshot of the visible viewport, rendered DOM-based in the browser
(
html-to-image) with a loading indicator during capture; the widget itself is never part of the screenshot - Annotation editor on
Fabric.js: freehand, rectangle, arrow, text, undo/redo, remove selection, five colors - Direct task creation in a fixed Asana project; the target section is
resolved by name (configurable candidate list, e.g.
Todo) or by fixed GID - Every user can set an optional task title (otherwise the task is named
Website-Feedback: <description>) and assign the task to a client visible assignee (visibleToClient: true); submissions without an explicit choice use the configured default assignee - Logged-in Neos users are identified server side; their display name is used as author and cannot be overridden by the browser
- Members of the internal Code Q team (server side allowlist) can pick every configured assignee and get the Asana task link after submission
- Feedback button in the Neos backend toolbar (next to the dimension switcher) for all logged-in users; its screenshot captures the full backend including the content canvas and inspector, and its technical context includes the live content-canvas URL
- Optional screencast recording (Screen Capture API, https only), attached to the same task
- German and English UI via XLIFF resources, following the site language
- Styled after the Neos CMS backend and hardened against site CSS
- Rate limiting, server side MIME/size validation and cleanup of temp files
The Neos package never talks to Asana directly and never holds the Asana
personal access token. Instead it posts every submission to a small
standalone relay script (see RemoteService/) that is
deployed once on a central server, e.g. at
https://docs.codeq.at/asana-feedback/:
Browser widget ──▶ Neos (CodeQ.AsanaFeedback) ──▶ relay (docs.codeq.at) ──▶ Asana API
holds: shared secret holds: Asana token
The Neos project authenticates against the relay with a shared secret
(ASANA_FEEDBACK_ACCESS_TOKEN). The relay accepts exactly one operation —
create one feedback task with its attachments — so a compromised website
cannot do anything else with the Asana account (no reading tasks, no other
projects beyond the optional allowlist, no deleting).
The relay must be running before any website can submit feedback. Copy the
contents of RemoteService/ (index.php, .htaccess,
config.example.php) into a folder on the central server, e.g.
docs.codeq.at/asana-feedback/, then:
- Copy
config.example.phptoconfig.php(git-ignored, never committed) and fill in:asanaAccessToken: the Personal Access Token of the dedicated Asana integration user (Asana developer console). The integration user must be a member of every target Asana project.sharedSecret: a strong random value, e.g.openssl rand -hex 32. This is the value the websites use asASANA_FEEDBACK_ACCESS_TOKEN.allowedProjectGids(optional): allowlist of project GIDs the relay may create tasks in; empty allows every project of the integration user.
- Make sure
config.phpis not served: the shipped.htaccessdenies it on Apache; on nginx add an equivalentlocationblock. - The relay must be reachable via HTTPS only.
- Allow large uploads: screencasts can be up to 100 MB, so
upload_max_filesize/post_max_sizeneed ~128M andmax_execution_time~300s (the.htaccesssets this for mod_php, otherwise configure it in the PHP pool).
A quick smoke test — an unauthenticated request must return 401:
curl -i -X POST https://docs.codeq.at/asana-feedback/Install the package from Packagist in the project root:
composer require codeq/asanafeedbackNothing else needs to be wired up manually: the Fusion integration
(autoInclude), the routes, the security policy for the public endpoint,
the authentication request pattern and the Neos backend toolbar plugin are
all registered by the package itself.
Provide the relay's shared secret (the sharedSecret from the relay's
config.php, not an Asana token) as the environment variable
ASANA_FEEDBACK_ACCESS_TOKEN — never in versioned configuration. Locally
with ddev, for example:
# .ddev/config.local.yaml (git-ignored)
web_environment:
- ASANA_FEEDBACK_ACCESS_TOKEN=3f9c2e...followed by ddev restart. On Proserver/Beach the variable is set through
the deployment secret store.
If the relay runs somewhere other than the default
https://docs.codeq.at/asana-feedback/, point the package at it:
# DistributionPackages/Vendor.Site/Configuration/Settings.AsanaFeedback.yaml
CodeQ:
AsanaFeedback:
feedbackService:
endpoint: 'https://example.com/asana-feedback/'The only mandatory per-project setting is the Asana project GID (the long number in the project URL):
# DistributionPackages/Vendor.Site/Configuration/Settings.AsanaFeedback.yaml
CodeQ:
AsanaFeedback:
asanaProjectGid: '1216274953146548'Tasks are placed in the first section whose name matches the configured
candidate list (Todo, Todos, Organisation — case-insensitive). Make
sure the Asana project has such a section, configure your own
asanaSectionNames, or pin a fixed asanaSectionGid. If no section can be
resolved, submissions fail with a controlled error message.
enableInFrontend controls whether the widget is rendered on the website
for all visitors. The package default is false, but it ships context
configuration that enables it in the Development,
Production/Proserver/Staging and Production/Beach/Staging Flow contexts.
Projects can override this per context in their global configuration, e.g.:
# Configuration/Production/Proserver/Staging/Settings.yaml
CodeQ:
AsanaFeedback:
enableInFrontend: falseThe decision is cached with the page (disabled sites stay fully cacheable),
so changing it requires a content cache flush:
./flow flow:cache:flushone Neos_Fusion_Content.
Independent of this flag, every logged-in Neos user always has the feedback button in the backend toolbar.
Frontends that do not render the Fusion integration can request the same safe
frontend configuration from
/codeq-asana-feedback/frontend-config?locale=en. The endpoint is available
whenever enableInFrontend permits the current visitor to use the widget; the
authenticated backend configuration endpoint remains unchanged.
Embed the returned JSON as the text content of a
#codeq-asana-feedback-config script element, then load
Public/Styles/Widget.css and Public/Scripts/Widget.js. Hosts whose visible
content lives in same-origin iframes can add
data-include-iframes="true" to the configuration element so the iframe
content is composited into the screenshot.
The package defaults (see Configuration/Settings.yaml) already contain the
Code Q team mapping; every value can be overridden per project:
CodeQ:
AsanaFeedback:
enableInFrontend: false
feedbackService:
# relay service that holds the actual Asana access token
endpoint: 'https://docs.codeq.at/asana-feedback/'
# shared secret to authenticate against the relay
accessToken: '%env:ASANA_FEEDBACK_ACCESS_TOKEN%'
asanaProjectGid: ''
defaultAssigneeGid: '422230010221' # Roland; used when none is selected
# optional fixed section; when empty the section is resolved by name:
asanaSectionGid: ''
asanaSectionNames: ['Todo', 'Todos', 'Organisation']
limits:
screenshotBytes: 10485760 # 10 MB
videoBytes: 100000000 # Asana attachment limit
descriptionCharacters: 10000
rateLimit: # per client IP
maxPerMinute: 5
maxPerHour: 40
# Neos account identifiers of the internal team: these users can pick
# every assignee, get the task link and are named by their Neos account
teamAccountIdentifiers:
- 'roland.schuetz'
- 'felix.gradinaru'
- 'daniel.schmelz'
- 'clara.borek'
- 'michael.koepl'
- 'dion.holder'
# selectable assignees; "visibleToClient: true" entries can be picked
# by every visitor, the others only by team members
assignees:
roland:
label: 'Roland'
asanaUserGid: '422230010221'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/roland.jpg'
visibleToClient: true
yurii:
label: 'Yurii'
asanaUserGid: '510973132418883'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/yurii.jpg'
visibleToClient: false
daniel:
label: 'Daniel'
asanaUserGid: '240050036824008'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/daniel.jpg'
visibleToClient: false
clara:
label: 'Clara'
asanaUserGid: '1210155270823988'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/clara.jpeg'
visibleToClient: true
michael:
label: 'Michael'
asanaUserGid: '778955489601506'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/michael.jpeg'
visibleToClient: true
arvin:
label: 'Arvin'
asanaUserGid: '1213418029127040'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/arvin.jpeg'
visibleToClient: false
dion:
label: 'Dion'
asanaUserGid: '1215306059024724'
avatar: 'resource://CodeQ.AsanaFeedback/Public/Images/Team/dion.jpg'
visibleToClient: true- The Asana personal access token only exists on the central relay server; the websites merely hold a shared secret whose only capability is creating one feedback task per request. A compromised website cannot read, modify or delete anything in Asana.
- All Asana communication happens exclusively server side; secret, project, section and assignee GIDs are never delivered to the browser
- The relay re-validates everything independently: constant time secret comparison, numeric GIDs, optional project allowlist and content sniffed MIME types of the attachments
- Submitted assignees are validated server side against the allowlist and
the
visibleToClientflag; project and section can never be chosen by the client - The submit endpoint is rate limited per client IP and validates MIME type (content sniffing), file size and description length server side
- Uploaded files are stored under server generated temporary names and are removed after the transfer, successful or not
The built assets are committed under Resources/Public, deployments need no
node step. To rebuild after changes (builds the website widget and the
backend toolbar plugin):
cd Resources/Private/JavaScript && npm install
cd ../BackendUi && npm install && cd ../JavaScript
npm run buildTests:
# unit tests (from the distribution root)
bin/phpunit --bootstrap Build/BuildEssentials/PhpUnit/UnitTestBootstrap.php \
DistributionPackages/CodeQ.AsanaFeedback/Tests/Unit
# end-to-end tests in Chromium, Firefox and WebKit incl. Asana verification
# (ASANA_ACCESS_TOKEN is a real Asana PAT used to verify the created tasks,
# independent of the relay shared secret the website itself uses)
cd Tests/E2E && npm install playwright && npx playwright install
ASANA_ACCESS_TOKEN=... node run-tests.mjs| Library | License | Purpose |
|---|---|---|
| fabric 6.x | MIT | screenshot annotation canvas |
| html-to-image 1.x | MIT | DOM based screenshot rendering |
| esbuild 0.25.x | MIT | build tooling (dev only) |
| @neos-project/neos-ui-extensibility 8.x | GPL-3.0 (as Neos UI) | backend toolbar plugin shim (dev only, aliases to host UI) |
| Feather Icons (inlined SVG paths) | MIT | toolbar and status icons |
Versions are pinned via the committed package-lock.json; third-party license
texts are linked from the bundle header comment (Widget.js legal comments).