codeq / asanafeedback
Feedback widget for Neos CMS websites that creates Asana tasks with annotated screenshots
Package info
github.com/code-q-web-factory/CodeQ.AsanaFeedback
Language:JavaScript
Type:neos-package
pkg:composer/codeq/asanafeedback
Requires
- php: >=8.1
- ext-curl: *
- ext-fileinfo: *
- neos/neos: ^8.0
README
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.
Features
- 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
Architecture: the Asana feedback relay
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).
Setup
0. Deploy the relay service (once, centrally)
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/
1. Require the package
Install the package from Packagist in the project root:
composer require codeq/asanafeedback
Nothing 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.
2. Provide the relay shared secret
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/'
3. Configure the Asana project
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.
4. Decide where the frontend widget is visible
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: false
The 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.
Static and headless frontend embeds
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.
All configuration options
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
Security notes
- 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
Development
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 build
Tests:
# 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
Open source dependencies
| 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).