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

  1. Click the round Agentation button in the bottom-right corner to start annotating.
  2. Click anything on this page, such as a card below, type what should change and press Add.
  3. 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.

ParameterEffectDefault
appName in the report header, the email subject and AgentationTestApp
hideDrawer items to remove, comma separated: whatsapp, telegram, email, download. Copy and Clear all always show.none
hide_all_feedback_buttontrue removes the tab; Agentation staysfalse
post_urlAdds 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_tokenSent as Authorization: Bearer <token> with each postnot set
post_labelLabel of the post button in the drawer and the Clear all dialogPost 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
…

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" } }
]

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.

FieldMeaning
hashSHA-256 hex of app + origin + deviceId + agentation.id; the upsert key
deletedfalse for a live note
app, deviceId, originThe app parameter, a random id kept per browser, and the site's origin
sessionId, publicIpThe session ID at send time (changing it re-sends the same hash), and the tester's public IP or null
urlFull address of the page the note was made on, with query and fragment
path, query, fragmentIts parts: /orders, ?date=2026-10-01 (or empty), #/orders/42 on hash-routed apps (or empty)
userAgent, viewport, devicePixelRatio, sentAtBrowser, width×height, pixel ratio and ISO time, all at send time
agentationThe note exactly as Agentation stores it. Its fields follow.
agentation.id, .timestampAgentation's creation time in ms (string and number). id is unique only per browser, hence hash.
commentThe tester's feedback
element, elementPath, fullPathReadable element name, short CSS path, full DOM path
x, y, boundingBoxMarker at % from left and px from top; element box {x, y, width, height}
selectedText, nearbyTextText selected, or text near the element
cssClasses, computedStyles, accessibility, nearbyElements, attributesElement details
reactComponents, sourceFileReact component tree and source file, when detectable
isMultiSelect, elementBoundingBoxes, isFixed, frameMulti-select and its boxes; position-fixed element; iframe path for notes inside an iframe
kind, placement, rearrange, drawingIndexfeedback, placement or rearrange, with details for layout notes and drawings
intent, severity, status, threadfix/change/question/approve; blocking/important/suggestion; pending/acknowledged/resolved/dismissed; replies
url, createdAt, updatedAt, resolvedAt, resolvedBy, authorId, sessionIdAgentation'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