| name | he4rt-twitter-engagement-tracker |
| description | Chrome extension that passively captures X/Twitter GraphQL responses to track community engagement and export structured JSON for analytics ingestion. |
| triggers | ["track twitter engagement for my community","capture x.com graphql responses for analytics","set up twitter community engagement tracking","export twitter engagement data to json","monitor twitter followers and likes passively","integrate twitter analytics with my backend","build chrome extension for twitter tracking","analyze twitter community engagement metrics"] |
He4rt Twitter Engagement Tracker
Skill by ara.so — Marketing Skills collection.
Browser extension that passively captures X/Twitter GraphQL responses to track community engagement. Intercepts native Twitter API calls to extract tweets, likes, replies, and user interactions without hitting rate limits. Exports structured JSON for backend ingestion.
What It Does
- Passive capture: Intercepts GraphQL responses as you browse X/Twitter normally
- Community tracking: Monitors tweets, replies, likes, and engagement for specific handles
- Zero rate limits: Uses data the browser already receives, no additional API calls
- Structured export: Produces clean JSON with tweets, metrics, favoriters, and engagement summaries
- Backend-ready: Designed for ingestion into Laravel/database systems
Key GraphQL Endpoints Captured
| Endpoint | Data Extracted |
|---|
UserTweets | All tweets from tracked handle with full metrics |
Favoriters | Users who liked specific tweets (with mutual follow status) |
TweetDetail | Reply threads and engagement details |
UserByScreenName | Profile data (followers, bio, verification) |
Installation
Load the Extension
- Clone or download this project
- Open Chrome/Brave and navigate to
chrome://extensions/
- Enable Developer mode (top right toggle)
- Click Load unpacked and select the project directory
- Pin the extension icon to your toolbar for easy access
Project Structure
he4rt-analytics/
├── manifest.json # Manifest V3 config
├── interceptor.js # MAIN world - patches window.fetch()
├── content.js # ISOLATED world - bridge to background
├── background.js # Service worker - data processing
├── popup.html/css/js # Extension UI
└── icons/ # Extension icons (16, 48, 128px)
Key Concepts
Three-Context Architecture
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args).then(response => {
const cloned = response.clone();
cloned.json().then(data => {
window.postMessage({
type: 'TWITTER_GRAPHQL_RESPONSE',
url: args[0],
data: data
}, '*');
});
return response;
});
};
window.addEventListener('message', (event) => {
if (event.data.type === 'TWITTER_GRAPHQL_RESPONSE') {
chrome.runtime.sendMessage({
type: 'GRAPHQL_INTERCEPTED',
payload: event.data
});
}
});
chrome.runtime.onMessage.( {
(message. === ) {
(message.);
}
});
Configuration
manifest.json
{
"manifest_version": 3,
"name": "He4rt Analytics",
"version": "1.0",
"permissions": [
"storage",
"activeTab"
],
"host_permissions": [
"https://x.com/*",
"https://twitter.com/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://x.com/*", "https://twitter.com/*"],
"js": ["content.js"],
"run_at":
Setting Tracked Handle
async function setTrackedHandle(screenName) {
await chrome.storage.local.set({
trackedHandle: screenName.replace('@', '')
});
}
async function getTrackedHandle() {
const result = await chrome.storage.local.get(['trackedHandle']);
return result.trackedHandle || null;
}
Core Implementation
Intercepting GraphQL Responses (interceptor.js)
(function() {
const originalFetch = window.fetch;
window.fetch = function(...args) {
const url = args[0];
return originalFetch.apply(this, args).then(response => {
if (typeof url === 'string' && url.includes('api.x.com/graphql')) {
const clonedResponse = response.clone();
clonedResponse.json().then(data => {
const endpoint = url.match(/\/graphql\/[^\/]+\/([^?]+)/)?.[1] || 'unknown';
window.postMessage({
type: 'TWITTER_GRAPHQL_RESPONSE',
url: url,
endpoint: endpoint,
data: data,
timestamp: Date.now()
}, );
}).( {
});
}
response;
});
};
})();
Bridging to Background (content.js)
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type !== 'TWITTER_GRAPHQL_RESPONSE') return;
chrome.runtime.sendMessage({
type: 'GRAPHQL_INTERCEPTED',
payload: event.data
});
});
chrome.runtime.sendMessage({
type: 'GRAPHQL_INTERCEPTED',
payload: event.data
}, (response) => {
console.log('Data forwarded to background:', response);
});
Processing and Storage (background.js)
let capturedTweets = new Map();
let capturedFavoriters = new Map();
let capturedReplies = [];
let trackedHandle = null;
chrome.storage.local.get(['trackedHandle'], (result) => {
trackedHandle = result.trackedHandle;
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GRAPHQL_INTERCEPTED') {
const { endpoint, data, url } = message.payload;
switch(endpoint) {
case 'UserTweets':
processTweets(data, url);
break;
case 'Favoriters':
processFavoriters(data, url);
break;
case 'TweetDetail':
processReplies(data);
break;
case 'UserByScreenName':
processUserProfile(data);
;
}
({ : });
}
;
});
() {
instructions = data?.?.?.?.?.?. || [];
( instruction instructions) {
(instruction. === ) {
( entry instruction. || []) {
tweet = (entry);
(tweet && (tweet)) {
capturedTweets.(tweet., tweet);
}
}
}
}
}
() {
content = entry.?.?.?.;
(!content) ;
legacy = content.;
(!legacy) ;
user = content.?.?.?.;
screenName = user?.;
{
: content.,
: legacy.,
: legacy.,
: screenName,
: {
: legacy.,
: legacy.,
: legacy.,
: legacy.,
: legacy.,
: (content.?. || )
},
: (legacy.?. || []).( h.),
: (legacy.?. || []).( ({
: m.,
: m.
})),
: (legacy.?. || []).,
: !!legacy.,
: legacy.
};
}
() {
trackedHandle &&
tweet.?.() === trackedHandle.();
}
() {
tweetId = url.()?.[];
(!tweetId) ;
entries = data?.?.?.?.?.[]?. || [];
users = [];
( entry entries) {
user = entry.?.?.?.;
(!user) ;
legacy = user.;
users.({
: user.,
: legacy.,
: legacy.,
: legacy.,
: legacy.,
: legacy.,
: user. || legacy.
});
}
(!capturedFavoriters.(tweetId)) {
capturedFavoriters.(tweetId, []);
}
capturedFavoriters.(tweetId).(...users);
}
Building Export JSON
function buildExportJSON() {
const tweets = Array.from(capturedTweets.values());
const originalTweets = tweets.filter(t => !t.is_retweet);
const retweets = tweets.filter(t => t.is_retweet);
const favoritersByTweet = {};
capturedFavoriters.forEach((users, tweetId) => {
favoritersByTweet[tweetId] = users;
});
const totalLikes = originalTweets.reduce((sum, t) => sum + t.metrics.favorite_count, 0);
const totalViews = originalTweets.reduce((sum, t) => sum + t.metrics.view_count, 0);
const topTweetByLikes = originalTweets.sort((a, b) =>
b.metrics.favorite_count - a.metrics.favorite_count
)[0]?.tweet_id;
const topTweetByViews = originalTweets.(
b.. - a..
)[]?.;
uniqueEngagers = ();
capturedFavoriters.( {
users.( uniqueEngagers.(u.));
});
capturedReplies.( uniqueEngagers.(r..));
{
: {
: trackedHandle,
},
: ().(),
: tweets,
: capturedReplies,
: favoritersByTweet,
: {
: tweets.,
: originalTweets.,
: retweets.,
: capturedReplies.,
: totalLikes,
: totalViews,
: originalTweets. > ?
.(totalLikes / originalTweets.) : ,
: originalTweets. > ?
.(totalViews / originalTweets.) : ,
: uniqueEngagers.,
: topTweetByLikes,
: topTweetByViews
}
};
}
() {
json = ();
blob = ([.(json, , )], {
:
});
url = .(blob);
filename = ;
chrome..({
: url,
: filename,
:
});
}
Popup UI Implementation
popup.js - UI Logic
document.addEventListener('DOMContentLoaded', async () => {
const handleInput = document.getElementById('handle-input');
const trackBtn = document.getElementById('track-btn');
const exportBtn = document.getElementById('export-btn');
const statsDiv = document.getElementById('stats');
const { trackedHandle } = await chrome.storage.local.get(['trackedHandle']);
if (trackedHandle) {
handleInput.value = trackedHandle;
loadStats();
}
trackBtn.addEventListener('click', async () => {
const handle = handleInput.value.trim().replace('@', '');
if (!handle) return;
await chrome.storage.local.set({ trackedHandle: handle });
chrome..({
: ,
: handle
});
trackBtn. = ;
( trackBtn. = , );
();
});
exportBtn.(, {
chrome..({ : }, {
(response?.) {
exportBtn. = ;
( exportBtn. = , );
}
});
});
() {
response = chrome..({ : });
statsDiv. = ;
}
(loadStats, );
});
Backend Integration
Laravel Ingestion Example
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\TwitterAccount;
use App\Models\Tweet;
use App\Models\CommunityEngagement;
class IngestTwitterAnalytics extends Command
{
protected $signature = 'twitter:ingest {file}';
protected $description = 'Ingest Twitter analytics JSON export';
public function handle()
{
$path = $this->argument('file');
if (!file_exists($path)) {
$this->error("File not found: {$path}");
return 1;
}
$data = json_decode(file_get_contents(), );
= ::(
[ => [][]],
[
=> [][] ?? ,
=> [][] ?? ,
=> [][] ?? ,
=> [][] ?? ,
]
);
([] ) {
::(
[ => []],
[
=> ->id,
=> [],
=> [],
=> [][],
=> [][],
=> [][],
=> [][],
=> [],
=> ([]),
]
);
}
([] => ) {
( ) {
::(
[
=> ,
=> [],
=> ,
],
[
=> [],
=> [],
=> [],
=> [] && [],
=> [],
]
);
}
}
([] ) {
::(
[
=> [],
=> [][],
=> ,
],
[
=> [][],
=> [],
]
);
}
->();
->();
;
}
}
Node.js/Express API Endpoint
const express = require('express');
const app = express();
app.post('/api/analytics/ingest', express.json({ limit: '50mb' }), async (req, res) => {
const data = req.body;
try {
const account = await db.twitterAccounts.upsert({
where: { screen_name: data.tracked_account.screen_name },
update: data.tracked_account,
create: data.tracked_account
});
const tweets = data.tweets.map(t => ({
...t,
account_id: account.id,
metrics: JSON.stringify(t.metrics),
hashtags: JSON.stringify(t.hashtags)
}));
await db.tweets.createMany({
data: tweets,
skipDuplicates: true
});
engagements = [];
( [tweetId, users] .(data.)) {
users.( {
engagements.({
: tweetId,
: user.,
: user.,
: ,
: user. && user.
});
});
}
db..({
: engagements,
:
});
res.({
: ,
: data.
});
} (error) {
.(, error);
res.().({ : error. });
}
});
Common Usage Patterns
Pattern 1: Daily Engagement Snapshot
chrome.alarms.create('daily-export', {
when: Date.now() + 1000,
periodInMinutes: 1440
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'daily-export') {
const json = buildExportJSON();
fetch('https://api.heartdevs.com/analytics/ingest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.API_KEY}`
},
body: JSON.stringify(json)
});
capturedTweets.clear();
capturedFavoriters.clear();
capturedReplies = [];
}
});
Pattern 2: Multi-Handle Tracking
let trackedHandles = new Set();
async function addTrackedHandle(handle) {
const handles = await chrome.storage.local.get(['trackedHandles']) || { trackedHandles: [] };
handles.trackedHandles.push(handle);
await chrome.storage.local.set({ trackedHandles: handles.trackedHandles });
trackedHandles = new Set(handles.trackedHandles);
}
function shouldTrack(tweet) {
return trackedHandles.has(tweet.author_screen_name?.toLowerCase());
}
function buildExportJSON(handle) {
const tweets = Array.from(capturedTweets.values())
.filter(t => t.author_screen_name?.toLowerCase() === handle.toLowerCase());
}
Pattern 3: Real-time Metrics Display
async function renderTweetFeed() {
const response = await chrome.runtime.sendMessage({ type: 'GET_RECENT_TWEETS', limit: 10 });
const feedHtml = response.tweets.map(tweet => `
<div class="tweet-card">
<div class="tweet-text">${escapeHtml(tweet.text)}</div>
<div class="tweet-metrics">
<span>❤️ ${tweet.metrics.favorite_count}</span>
<span>🔄 ${tweet.metrics.retweet_count}</span>
<span>👁️ ${tweet.metrics.view_count}</span>
</div>
</div>
`).join('');
document.getElementById('tweet-feed').innerHTML = feedHtml;
}
Troubleshooting
Extension Not Capturing Data
Check injection:
console.log(window.fetch.toString());
Verify content script injection:
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
console.log('Current URL:', tabs[0].url);
});
Storage permissions:
chrome.storage.local.set({ test: 'value' }, () => {
chrome.storage.local.get(['test'], (result) => {
console.log('Storage test:', result.test);
});
});
GraphQL Responses Not Parsing
Log raw responses:
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'GRAPHQL_INTERCEPTED') {
console.log('Raw endpoint:', message.payload.endpoint);
console.log('Raw data:', JSON.stringify(message.payload.data, null, 2));
}
});
Twitter API structure changes:
function safeExtract(obj, path, defaultValue = null) {
try {
return path.split('.').reduce((acc, part) => acc?.[part], obj) ?? defaultValue;
} catch {
return defaultValue;
}
}
const tweetText = safeExtract(entry, 'content.itemContent.tweet_results.result.legacy.full_text', '');
Export File Too Large
Implement pagination:
function buildExportJSON(page = 1, pageSize = 100) {
const tweets = Array.from(capturedTweets.values());
const start = (page - 1) * pageSize;
const end = start + pageSize;
return {
page: page,
total_pages: Math.ceil(tweets.length / pageSize),
tweets: tweets.slice(start, end),
};
}
Compression:
import pako from 'pako';
function downloadCompressedJSON() {
const json = JSON.stringify(buildExportJSON());
const compressed = pako.gzip(json);
const blob = new Blob([compressed], { type: 'application/gzip' });
}
Performance Issues
Debounce storage writes:
let saveTimeout;
function debouncedSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
chrome.storage.local.set({
tweets: Array.from(capturedTweets.entries()),
favoriters: Array.from(capturedFavoriters.entries())
});
}, 5000);
}
Memory management:
const MAX_TWEETS = 1000;
function addTweet(tweet) {
if (capturedTweets.size >= MAX_TWEETS) {
const oldest = Array.from(capturedTweets.keys())[0];
capturedTweets.delete(oldest);
}
capturedTweets.set(tweet.tweet_id, tweet);
}
Advanced Features
Webhook Auto-Push
const WEBHOOK_URL = 'https://api.heartdevs.com/webhook/twitter';
async function pushToWebhook(data) {
try {
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': await getApiKey()
},
body: JSON.stringify(data)
});
if (!response.ok) {
console.error('Webhook failed:', await response.text());
}
} catch (error) {
console.error('Webhook error:', error);
}
}
function processTweets(data, url) {
const tweets = extractTweetsFromData(data);
tweets.forEach(tweet => {
if (!capturedTweets.has(tweet.tweet_id)) {
capturedTweets.(tweet., tweet);
({
: ,
: tweet,
: ().()
});
}
});
}
Engagement Scoring
function calculateEngagementScore(tweet) {
const weights = {
like: 1,
retweet: 5,
reply: 10,
quote: 15
};
const rawScore =
(tweet.metrics.favorite_count * weights.like) +
(tweet.metrics.retweet_count * weights.retweet) +
(tweet.metrics.reply_count * weights.reply) +
(tweet.metrics.quote_count * weights.quote);
const normalizedScore = rawScore / Math.log10(tweet.author_followers_count + 10);
return Math.round(normalizedScore * 100) / 100;
}
function buildExportJSON() {