A Chrome browser extension that injects customizable annotation surveys directly into social media feeds β so researchers can label content in context, without leaving the platform.
Human-labeled data is at the core of computational social science and content moderation research. Social Annotate eliminates the friction of switching between a platform and a separate labeling tool: annotators see real posts in their native interface, answer a configurable survey inline, and their labels are stored locally and optionally sent to a research server in real time.
| Platform | Post Annotation | User/Profile Annotation |
|---|---|---|
| X / Twitter | β | β |
| β | β | |
| Bluesky | β | β |
| WhatsApp Web | β | β |
| Telegram Web | β | β |
| β | β | |
| Truth Social | β | β |
- In-feed surveys β annotation forms appear directly alongside posts; no copy-pasting, no context switching.
- Fully configurable questions β build survey forms visually or write raw JSON. Supports radio buttons, sliders, text inputs, and checkboxes.
- Per-survey informed consent β write IRB consent text in Markdown per survey. When enabled, a full-screen consent overlay blocks annotation until the participant approves β and a timestamped consent record is automatically saved to disk for legal compliance.
- Guided annotation mode β upload a target list (post IDs or usernames); the extension navigates annotators through the list in order and tracks progress.
- Media downloads β optionally save post images, videos, profile pictures, and banners alongside labels, organized into a consistent folder structure.
- JSONL export β download all collected labels from the popup in one click, or stream them to an API endpoint on every submission.
- Config import / export β share a study configuration as a single JSON file across your team.
- Light and dark themes β both the extension UI and injected survey forms support user-toggleable themes.
- Self-healing selectors β an accompanying Python agent pipeline detects when platform DOM changes break injection and proposes updated CSS selectors automatically.
Social Annotate is distributed as an unpacked Chrome extension (Chrome MV3).
- Clone or download this repository.
- Open Chrome and navigate to
chrome://extensions. - Enable Developer mode (toggle in the top-right corner).
- Click Load unpacked and select the
src/folder inside this repository. - The Social Annotate icon will appear in your Chrome toolbar.
Reload the extension from
chrome://extensionsany time you change source files.
- Click the Social Annotate icon in the toolbar to open the popup.
- Select an active survey from the dropdown (default:
x-post). - Navigate to the corresponding platform β a survey form will appear next to each post.
- Fill in the survey and click Submit. The annotation counter in the popup increments.
- Click Export in the popup footer to download your labels as a
.jsonlfile.
| Control | Description |
|---|---|
| Active Survey dropdown | Switches which survey type is active (e.g. x-post, bluesky-user). Only one survey is active at a time to prevent duplicate forms. |
| Extension toggle | Globally enables or disables injection without uninstalling. |
| Guided Mode toggle | Activates target-list navigation. The progress bar and Prev / Next controls appear. |
| Download Media toggle | (Post surveys) Saves post images and videos to disk on each submission. |
| Download Profile Picture / Banner toggles | (User surveys) Saves avatar and banner images on each submission. |
| Export button | Downloads all collected annotations for the active survey as a .jsonl file. |
| βοΈ icon | Opens the Options page. |
| βοΈ / π icon | Toggles light/dark theme. |
Open the Options page via the βοΈ button in the popup, or by right-clicking the extension icon and selecting Options.
| Field | Description |
|---|---|
| API Endpoint | If set, every annotation submission is also POSTed to this URL as JSON (e.g. http://127.0.0.1:5000/response). |
| Downloads Folder | Replaces SocialAnnotateExports/ as the root folder for all exports and media downloads. Leave blank to use the default. |
Each supported survey has its own card. Cards are collapsed by default; click the header to expand. Each card has three tabs:
| Field | Description |
|---|---|
| Study ID | An identifier for your study, written into consent records and annotation files (e.g. hate_speech_2025). |
| Insert Location | The HTML element name where the survey is injected (user surveys only; post surveys use a MutationObserver). |
| Annotation List | Target post IDs or usernames for guided mode. Accepts comma-separated values or a .txt / .csv file. |
| Survey Theme | Light or Dark β controls the visual style of the injected survey form. |
Toggle Enable Consent Popup to require participants to read and approve an informed consent statement before they can annotate on that platform.
The consent text is written in Markdown and rendered in a live side-by-side editor. Use {platform} as a placeholder for the platform name. A default IRB-style template is pre-filled.
When a participant clicks Approve:
- Their consent is stored in the extension so the overlay does not reappear.
- A JSON consent record is automatically downloaded to
{Downloads Folder}/consent_records/{platform}_{survey_type}_{unix_timestamp}.json.
The consent record contains: timestamp (ISO 8601 + Unix), platform, survey type, study ID, anonymous client ID, the exact consent text the participant saw (Markdown + rendered HTML), user agent, and extension version. This is sufficient documentation for most IRB requirements.
Configure the survey questions shown to annotators. Two editing modes:
- β‘ Visual β add, remove, and reorder fields using a drag-free builder. Supported field types: Radio Buttons, Range/Slider, Text Input, Checkbox.
- { } JSON β edit the raw jsonform schema directly for full control.
Switching between modes syncs the state in both directions.
| Button | Description |
|---|---|
| πΎ Save Changes | Persists all options to chrome.storage.local. The page reloads on next visit reflecting saved values. |
| β Export Config | Downloads the full configuration as config.json. |
| π Choose File + Import Config | Load a previously exported config.json to restore or share a study setup. |
| π Factory Reset | Erases all settings, annotations, and stored data. Requires confirmation. |
Forms are defined as jsonform schemas. Below is the default hate-speech example:
{
"schema": {
"hatespeech": {
"type": "string",
"title": "Does this text contain hate speech?",
"enum": ["Yes", "No"],
"required": true
}
},
"form": [
{ "key": "hatespeech", "type": "radiobuttons" },
{ "type": "submit", "title": "Submit", "htmlClass": "surveySubmitBtn" }
]
}type in form[] |
Description |
|---|---|
radiobuttons |
Horizontal button group, one selection |
range |
Numeric slider (uses minimum / maximum from schema) |
text |
Free-text input |
checkbox |
Boolean toggle |
Each annotation is one JSON object per line. Example record:
{
"surveyType": "x-post",
"hatespeech": "No",
"account_id": "elonmusk",
"post_id": "1234567890",
"survey_init_timestamp": 1748779200,
"submission_timestamp": 1748779250,
"clientID": "_lx3k1a-9f2zq"
}clientID is a pseudo-unique anonymous identifier generated at install time and stable across sessions β useful for multi-annotator studies.
{Downloads Folder or SocialAnnotateExports}/
βββ consent_records/
β βββ x_x-post_1748779200.json
βββ x/
β βββ x-post/
β βββ media/
β βββ pictures/
β β βββ elonmusk_1234567890_1.jpg
β βββ videos/
β βββ elonmusk_1234567890_1.mp4
βββ bluesky/
βββ bluesky-user/
βββ media/
βββ profile_pictures/
βββ profile_banner/
If an API Endpoint is configured, every submission also fires a POST request with the annotation JSON as the body (Content-Type: application/json). A minimal Flask receiver:
from flask import Flask, request
app = Flask(__name__)
@app.route('/response', methods=['POST'])
def receive():
print(request.json)
return '', 200
app.run(port=5000)Guided mode walks annotators through a pre-defined target list one item at a time.
- In the Annotation List field of the target survey, enter post IDs or usernames (comma-separated or uploaded from a file).
- Save options.
- Enable Guided Mode in the popup.
- The extension navigates to each target in order. The popup shows a progress bar and Prev / Next navigation.
- After every submission the extension automatically advances to the next target.
Each platform requires four additions:
| File | Purpose |
|---|---|
src/content-scripts/{platform}/inject.js |
Main content script: builds survey Context objects and injects <div> wrappers into the DOM using a MutationObserver. |
src/content-scripts/{platform}/inject.css |
Styles for the injected survey container. |
src/content-scripts/{platform}/inject-api.js |
(Optional) MAIN-world script for intercepting XHR/fetch to extract post metadata. |
src/config.js |
Add a new survey entry under config.surveys with socialMediaPlatform, studyID, surveyFormSchema, etc. |
src/manifest.json |
Add host_permissions and content_scripts entries for the new domain. |
src/selectors.json |
Add platform-specific CSS selectors used by inject.js and the health-check system. |
The shared.js Context class handles all the common logic: consent overlay, form rendering in a sandboxed iframe, submission routing, guided-mode advancement, and media download dispatch. Your inject.js only needs to call context.renderSurvey(userID, postID) at the right moment.
Social media platforms frequently change their HTML structure, which can silently break injection. The self-healing agent is a Python pipeline that detects broken CSS selectors and proposes replacements β automatically, using an LLM.
The agent runs an 11-step pipeline:
- Validates the HTML fixture offline (BeautifulSoup post count, missing asset warnings)
- Sends the HTML to an LLM (Claude or Gemini) to extract new CSS selectors
- Opens the fixture in a real Chromium instance with the extension loaded
- Waits for injection and scrolls to trigger the
MutationObserver - Takes a screenshot to visually confirm injection
- Verifies injection by counting survey containers and shadow DOM iframes
- Checks that survey forms are accessible inside the sandbox frames
- Fills and submits the first available form option
- Validates the submission (checks for the "Done!" button state)
- Writes the proposed selectors to a temp JSON file (never touches
src/selectors.jsondirectly) - Presents a diff against the current selectors for review
pip install anthropic playwright beautifulsoup4
playwright install chromiumSet your API key in the environment:
export ANTHROPIC_API_KEY=sk-... # Claude (checked first)
# or
export GEMINI_API_KEY=... # Gemini (fallback)Point the agent at a saved HTML fixture of the target platform:
# Inspect proposed selectors without applying them
python run_healer.py --file test_fixtures/x_twitter/x.html
# Retry LLM extraction up to 5 times
python run_healer.py --file test_fixtures/x_twitter/x.html --retries 5
# Apply the proposed selectors to src/selectors.json automatically
python run_healer.py --file test_fixtures/x_twitter/x.html --apply
# Skip the browser step β LLM extraction only (fast, offline)
python run_healer.py --file test_fixtures/x_twitter/x.html --llm-onlyPlatform is auto-detected from the filename. Override with --platform x if needed.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser Tab (e.g. x.com) β
β β
β inject.js + shared.js β
β ββ MutationObserver detects posts β
β ββ Injects <div> shadow host per post β
β ββ Shadow DOM contains sandboxed <iframe> β
β ββ sandbox/survey.html + jsonform β
β Posts messages β inject.js via postMessage β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β chrome.runtime.sendMessage
βββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β background.js (MV3 Service Worker) β
β ββ Handles: downloadMedia, exportAnnotations, β
β saveConsentRecord, postApi β
β ββ chrome.downloads.onDeterminingFilename β
β sets reliable subdirectory paths via #sa_fn= key β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β chrome.storage.local
βββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β Popup (popup.html / popup.js) β
β ββ Survey selector, toggles, export, guided nav β
β β
β Options Page (options.html / options.js) β
β ββ Global settings, per-survey config cards β
β ββ EasyMDE consent editor, visual form builder β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All persistent state lives in chrome.storage.local. The sandbox iframe is served from sandbox/survey.html and communicates with the parent content script exclusively via postMessage β it has no access to the page DOM or extension APIs.
If you use Social Annotate in your research, please cite:
@article{najafi2026socialannotate,
title = {Social-Annotate: Self-Healing Browser Extension to Annotate and Collect Social Media Data},
author = {Najafi, Ali and Varol, Onur and Uluturk, Ismail},
journal = {Journal of Open Source Software},
volume = {X},
number = {XX},
pages = {XXXX},
year = {2026}
}- Ali Najafi β najafi-ali.com Β· @najafialiai
- Ismail Uluturk β uluturki.github.io Β· @strictlynofun
- Onur Varol β onurvarol.com Β· @onurvarol
Issues and pull requests are welcome. For questions, reach out via the public profiles above.
GPL-3.0 β see LICENSE.