ETS Tech · internal tool
Feedback Annotation
Point at anything on a page, say what should change, and send every note from every page as one report. Built on Agentation, added with one script tag.
Try it on this page
- Click the round Agentation button in the bottom-right corner to start annotating.
- Click anything on this page, such as a card below, type what should change and press Add.
- A Send 1 Feedback tab appears on the right edge. Open it to share the report by WhatsApp, Telegram or Email, copy it, or download it.
Pricing
Plans start at ₹499 a month, billed yearly.
Sign up
Create an account in under a minute with your school email.
Support
Our team replies within one working day.
Add it to an app
Paste one tag into the page's HTML. It must be type="module".
<script type="module" src="https://cdn.jsdelivr.net/gh/ets-tech/feedback-annotation@1/feedback-annotation.min.js?app=MyApp"></script>@1 picks up every 1.x fix automatically and never a breaking 2.0. Drop .min from the file name for the readable version.
For dev and staging only. Don't ship the tag to production.
Parameters
Add them to the script URL.
| Parameter | Effect | Default |
|---|---|---|
app | Name in the report header, the email subject and Agentation | TestApp |
hide | Drawer items to remove, comma separated: whatsapp, telegram, email, download. Copy and Clear all always show. | none |
hide_all_feedback_button | true removes the tab; Agentation stays | false |
post_url | Adds Post to server to the drawer, which sends every note as JSON to this URL. URL-encode it. See Post to server. | not set |
post_token | Sent as Authorization: Bearer <token> with each post | not set |
post_label | Label of the post button in the drawer and the Clear all dialog | Post to server |
<!-- Email and Copy only -->
<script type="module" src="https://cdn.jsdelivr.net/gh/ets-tech/feedback-annotation@1/feedback-annotation.min.js?app=MyApp&hide=whatsapp,telegram"></script>Session ID and Download
The first note opens a popup asking for a session ID: a Jira ticket, a GitHub issue, or anything else that groups this round of feedback. Left blank, it becomes MyApp- plus 8 random characters, e.g. MyApp-e8c79f40. Change it any time from Session at the top of the drawer; every note takes the new ID.
Download in the drawer saves the report as a Markdown file named after the session ID, e.g. PROJ-123.md. Hand it to a developer, attach it to the ticket, or give it to an AI coding agent. Copy, Download and the share channels all send the same report:
I added 3 feedbacks on staging.example.com. Fix each one, then verify it in the running app:
**App:** MyApp
**Environment:**
- Session ID: PROJ-123
- Public IP: 203.0.113.7
- Viewport: 1440×900
…- The public IP comes from api.ipify.org, looked up once per page load. If that fails, the line reads
Unavailable. - Clear all removes the session ID, so the next note asks again.
- Remove the Download button with
hide=download.
Post to server
Set post_url and the drawer gets a Post to server button. It sends one POST with a JSON array: every note on every page, plus the notes deleted since the last successful post. Notes stay in the browser afterwards, so posting again re-sends them.
<script type="module" src="https://cdn.jsdelivr.net/gh/ets-tech/feedback-annotation@1/feedback-annotation.min.js?app=MyApp&post_url=https%3A%2F%2Fapi.example.com%2Ffeedback&post_token=abc123"></script>[
{
"hash": "3b7aad47…",
"deleted": false,
"app": "MyApp",
"deviceId": "e8c79f40-0b4e-4847-b5b7-f09ef45e5732",
"sessionId": "PROJ-123",
"publicIp": "203.0.113.7",
"origin": "https://staging.example.com",
"url": "https://staging.example.com/orders?date=2026-10-01#tab=2",
"path": "/orders",
"query": "?date=2026-10-01",
"fragment": "#tab=2",
"userAgent": "Mozilla/5.0 …",
"viewport": "1440×900",
"devicePixelRatio": 2,
"sentAt": "2026-10-26T10:00:00.000Z",
"agentation": {
"id": "1793012632233",
"comment": "Make the heading bigger",
"element": "h1",
"elementPath": "body > h1",
"timestamp": 1793012632233,
"…": "the other Agentation fields below"
}
},
{ "hash": "9c1e…", "deleted": true, "app": "MyApp", "deviceId": "e8c79f40-…", "sessionId": "PROJ-123", "deletedAt": "2026-10-26T09:58:12.000Z", "agentation": { "id": "1793000000000" } }
]hashis the SHA-256 ofapp + origin + deviceId + agentation.id. It stays the same when a note is edited, so upsert byhash.deviceIdis a random id kept per browser, so two testers' notes never share a hash.deleted: trueitems are notes the tester deleted. The server decides what to do with them; a hash it has never seen can be ignored.- Clear all first posts every note as deleted and clears nothing unless the server answers 2xx. If some notes were never posted, or were edited since, the dialog says so and offers Post to server.
- Any 2xx is success. A failure shows "Failed (status)" and logs the status and response body to the console.
Fields
Headers: Content-Type: application/json, plus Authorization: Bearer <token> when post_token is set. Optional Agentation fields are left out when empty, never sent as null.
| Field | Meaning |
|---|---|
hash | SHA-256 hex of app + origin + deviceId + agentation.id; the upsert key |
deleted | false for a live note |
app, deviceId, origin | The app parameter, a random id kept per browser, and the site's origin |
sessionId, publicIp | The session ID at send time (changing it re-sends the same hash), and the tester's public IP or null |
url | Full address of the page the note was made on, with query and fragment |
path, query, fragment | Its parts: /orders, ?date=2026-10-01 (or empty), #/orders/42 on hash-routed apps (or empty) |
userAgent, viewport, devicePixelRatio, sentAt | Browser, width×height, pixel ratio and ISO time, all at send time |
agentation | The note exactly as Agentation stores it. Its fields follow. |
agentation.id, .timestamp | Agentation's creation time in ms (string and number). id is unique only per browser, hence hash. |
comment | The tester's feedback |
element, elementPath, fullPath | Readable element name, short CSS path, full DOM path |
x, y, boundingBox | Marker at % from left and px from top; element box {x, y, width, height} |
selectedText, nearbyText | Text selected, or text near the element |
cssClasses, computedStyles, accessibility, nearbyElements, attributes | Element details |
reactComponents, sourceFile | React component tree and source file, when detectable |
isMultiSelect, elementBoundingBoxes, isFixed, frame | Multi-select and its boxes; position-fixed element; iframe path for notes inside an iframe |
kind, placement, rearrange, drawingIndex | feedback, placement or rearrange, with details for layout notes and drawings |
intent, severity, status, thread | fix/change/question/approve; blocking/important/suggestion; pending/acknowledged/resolved/dismissed; replies |
url, createdAt, updatedAt, resolvedAt, resolvedBy, authorId, sessionId | Agentation's sync metadata, when present. For the page, use the top-level url; for the session, the top-level sessionId. |
A deleted note carries only hash, deleted: true, app, deviceId, sessionId, deletedAt and agentation: { id }.
The server must answer the CORS preflight (Access-Control-Allow-Origin for the site and Access-Control-Allow-Headers: Authorization, Content-Type; no cookies are sent) and check the Origin header against the sites it expects.
The token sits in the page's HTML, so anyone who can open the page can read it. Make it write-only and good for this endpoint alone.
How it works
- Notes stay in the tester's own browser. Nothing is sent anywhere until they share, copy, download or post the report. The one outside call is a public IP lookup to api.ipify.org.
- The report covers every page of the app, grouped by page address (path, query and
#part), in Agentation's detailed "forensic" format. - Clear all removes every note on every page, after a confirmation.
- The tab follows Agentation's light and dark theme.
- Loads React 19.3.0 and Agentation 3.1.2 from esm.sh; this script itself comes from jsDelivr.