A Chrome Extension that extracts Contacts, Deals, and Tasks from ActiveCampaign CRM, stores them locally using Chrome storage, and displays them in a React-based popup dashboard.
- Data Extraction: Extract Contacts, Deals, and Tasks from ActiveCampaign CRM views
- Local Storage: Persistent storage using
chrome.storage.localwith deduplication - React Dashboard: Modern popup UI with tabs, search, and filtering
- Visual Feedback: Shadow DOM-based extraction indicator with progress states
- Export Options: Export data as CSV or JSON format
- Real-time Sync: Cross-tab synchronization via storage events
- Node.js 18+ and npm
- Google Chrome browser
# Navigate to extension directory
cd extension
# Install dependencies
npm install
# Build for production
npm run build
# Or run in development mode with hot reload
npm run dev- Open Chrome and navigate to
chrome://extensions/ - Enable "Developer mode" (toggle in top-right)
- Click "Load unpacked"
- Select the
distfolder from the project
- Navigate to ActiveCampaign (contacts, deals, or tasks view)
- Click the extension icon in Chrome toolbar
- Click "Extract Now" button
- View extracted data in the popup dashboard
- Search, filter, or delete records as needed
- Export data using the Export menu
extension/
├── manifest.json # Chrome Extension Manifest V3
├── package.json # Node dependencies
├── src/
│ ├── background/
│ │ └── service-worker.ts # Message routing, storage events
│ ├── content/
│ │ ├── index.ts # Content script orchestrator
│ │ ├── extractors/ # Entity-specific data harvesters
│ │ ├── detectors/ # View type detection
│ │ └── indicators/ # Shadow DOM status UI
│ ├── popup/
│ │ ├── App.tsx # Main React application
│ │ ├── components/ # UI components
│ │ └── hooks/ # Custom React hooks
│ └── shared/
│ ├── types.ts # TypeScript interfaces
│ ├── constants.ts # Configuration and selectors
│ ├── message-types.ts # Message passing contracts
│ └── storage-service.ts # Chrome storage wrapper
The extraction engine uses a multi-layered selector strategy for resilience:
- Primary:
data-*attributes (most stable, least likely to change) - Secondary: Semantic CSS classes (e.g.,
.contact-name,.deal-value) - Tertiary: Generic patterns with context (table rows, list items)
- CSS selectors are faster to evaluate
- Native browser API support via
querySelector - Easier to maintain and debug
- Sufficient for most extraction needs
Each data field has multiple selectors tried in order:
const CONTACT_SELECTORS = {
name: [
'[data-testid="contact-name"]', // Best: Test ID
'.contact-name-cell a', // Good: Semantic class
'td.name-column a', // Okay: Positional
'[class*="contactName"]' // Fallback: Partial match
],
// ... similar for other fields
};- Uses
waitForElement()with configurable timeout - MutationObserver for lazy-loaded content detection
- Retry logic with exponential backoff
interface ACStorageSchema {
contacts: ACContact[]; // Extracted contact records
deals: ACDeal[]; // Extracted deal records
tasks: ACTask[]; // Extracted task records
lastSync: number; // Unix timestamp of last extraction
syncInProgress: boolean; // Lock flag for race conditions
}
interface ACContact {
id: string; // Generated stable ID
name: string;
email: string;
phone: string;
tags: string[];
owner: string;
extractedAt: number; // Extraction timestamp
sourceUrl: string; // Page URL when extracted
}
interface ACDeal {
id: string;
title: string;
value: number;
currency: string;
pipeline: string;
stage: string;
primaryContact: string;
owner: string;
extractedAt: number;
sourceUrl: string;
}
interface ACTask {
id: string;
type: 'call' | 'email' | 'meeting' | 'todo';
title: string;
dueDate: string;
assignee: string;
linkedEntity: {
type: 'contact' | 'deal';
id: string;
name: string;
} | null;
extractedAt: number;
sourceUrl: string;
}Records are deduplicated by their id field:
- If record exists with same ID, compare
extractedAttimestamps - Newer extraction overwrites older data
- No duplicate records in storage
Multi-tab extraction is handled with:
syncInProgressflag in storage- Lock timeout (30 seconds) for stale locks
- Retry logic with configurable attempts
- Atomic read-modify-write pattern
┌─────────────────────────────────────────────────────────────┐
│ Chrome Extension │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────────────────┐ │
│ │ Popup (React)│◄───────►│ Service Worker │ │
│ │ │ Messages│ (Background) │ │
│ │ - Dashboard │ │ │ │
│ │ - Search │ │ - Message routing │ │
│ │ - Export │ │ - Storage coordination │ │
│ └───────────────┘ │ - Badge updates │ │
│ └───────────────────────────┘ │
│ │ │
│ │ Messages │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Content Script │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Contact │ │ Deal │ │ Task │ │ │
│ │ │ Harvester │ │ Harvester │ │ Harvester │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ View │ │ Extraction Indicator │ │ │
│ │ │ Detector │ │ (Shadow DOM) │ │ │
│ │ └─────────────┘ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ chrome.storage.local │ │
│ │ (Persistent Data) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
npm run devThe CRXJS Vite plugin enables hot reload during development.
- Popup: Right-click extension icon > "Inspect popup"
- Content Script: Use page DevTools, scripts appear under Sources
- Service Worker:
chrome://extensions/> extension card > "service worker" link
MIT