@stacksleuth/browser-extension
Version:
Comprehensive browser extension for real-time performance monitoring - DevTools integration, content script injection, tab performance tracking, and interactive performance visualization.
291 lines • 10.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserExtension = void 0;
const core_1 = require("@stacksleuth/core");
class BrowserExtension extends core_1.BaseAgent {
constructor(config = {}) {
super(config);
this.metrics = [];
this.tabData = new Map();
this.isActive = false;
this.config = {
enableContentScript: true,
enableDevTools: true,
enablePopup: true,
autoInject: true,
trackingDomains: [],
excludedDomains: ['chrome://'],
...config
};
}
startMonitoring() {
this.initialize();
}
stopMonitoring() {
this.destroy();
}
async initialize() {
if (!this.isExtensionEnvironment()) {
console.warn('StackSleuth Extension can only run in browser extension environment');
return;
}
console.log('Initializing StackSleuth Browser Extension...');
this.setupMessageHandling();
if (this.config.enableDevTools) {
this.setupDevToolsPanel();
}
if (this.config.enableContentScript) {
this.setupContentScript();
}
this.isActive = true;
}
isExtensionEnvironment() {
return typeof chrome !== 'undefined' && chrome.runtime && !!chrome.runtime.id;
}
setupMessageHandling() {
if (chrome.runtime && chrome.runtime.onMessage) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
this.handleContentScriptMessage(message, sender);
sendResponse({ status: 'received' });
});
}
// Handle tab updates
if (chrome.tabs && chrome.tabs.onUpdated) {
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
this.handleTabComplete(tabId, tab.url);
}
});
}
// Handle tab removal
if (chrome.tabs && chrome.tabs.onRemoved) {
chrome.tabs.onRemoved.addListener((tabId) => {
this.tabData.delete(tabId);
});
}
}
setupDevToolsPanel() {
// This would typically be called from devtools.js
if (chrome.devtools && chrome.devtools.panels) {
chrome.devtools.panels.create('StackSleuth', 'icons/icon48.png', 'devtools/panel.html', (panel) => {
console.log('StackSleuth DevTools panel created');
panel.onShown.addListener((window) => {
// Panel shown - start collecting detailed metrics
this.startDetailedProfiling();
});
panel.onHidden.addListener(() => {
// Panel hidden - reduce metric collection
this.stopDetailedProfiling();
});
});
}
}
setupContentScript() {
// This method would be called to prepare content script injection
if (chrome.scripting) {
// Inject content script into all applicable tabs
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
if (tab.id && tab.url && this.shouldInjectIntoTab(tab.url)) {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content-script.js']
});
}
});
});
}
}
shouldInjectIntoTab(url) {
// Check if URL should be excluded
for (const excluded of this.config.excludedDomains || []) {
if (url.startsWith(excluded)) {
return false;
}
}
// Check if URL is in tracking domains (if specified)
if (this.config.trackingDomains && this.config.trackingDomains.length > 0) {
return this.config.trackingDomains.some(domain => url.includes(domain));
}
return true;
}
handleContentScriptMessage(message, sender) {
const tabId = sender.tab?.id;
if (!tabId)
return;
// Store metric
this.metrics.push({
tabId,
url: sender.tab?.url || '',
timestamp: message.timestamp,
type: this.mapMessageTypeToMetricType(message.type),
data: message.data
});
// Update tab performance data
this.updateTabPerformanceData(tabId, message);
// Keep only recent metrics (last 1000)
if (this.metrics.length > 1000) {
this.metrics = this.metrics.slice(-1000);
}
}
mapMessageTypeToMetricType(messageType) {
switch (messageType) {
case 'performance-data':
return 'page-load';
case 'network-request':
return 'resource-load';
case 'user-interaction':
return 'user-interaction';
case 'error':
case 'console-log':
return 'error';
default:
return 'page-load';
}
}
updateTabPerformanceData(tabId, message) {
let tabData = this.tabData.get(tabId);
if (!tabData) {
tabData = {
tabId,
url: message.url || '',
loadTime: 0,
resources: [],
interactions: [],
errors: []
};
this.tabData.set(tabId, tabData);
}
switch (message.type) {
case 'performance-data':
if (message.data.loadTime) {
tabData.loadTime = message.data.loadTime;
}
if (message.data.resources) {
tabData.resources = message.data.resources;
}
if (message.data.memoryUsage) {
tabData.memoryUsage = message.data.memoryUsage;
}
break;
case 'user-interaction':
tabData.interactions.push({
type: message.data.type,
element: message.data.element,
timestamp: message.timestamp
});
break;
case 'error':
tabData.errors.push({
message: message.data.message,
stack: message.data.stack,
timestamp: message.timestamp
});
break;
}
}
handleTabComplete(tabId, url) {
// Tab loading complete - collect initial metrics
if (chrome.scripting && this.shouldInjectIntoTab(url)) {
chrome.scripting.executeScript({
target: { tabId },
func: this.collectInitialMetrics
});
}
}
collectInitialMetrics() {
// This function runs in the page context
const performanceData = {
loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
resources: performance.getEntriesByType('resource').map(entry => ({
name: entry.name,
type: entry.initiatorType || 'unknown',
loadTime: entry.duration,
size: entry.transferSize || 0
})),
memoryUsage: performance.memory ? {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize
} : undefined
};
// Send to background script
if (chrome.runtime) {
chrome.runtime.sendMessage({
type: 'performance-data',
data: performanceData,
timestamp: Date.now()
});
}
}
startDetailedProfiling() {
// Start collecting detailed performance metrics when devtools panel is open
console.log('Starting detailed profiling...');
}
stopDetailedProfiling() {
// Reduce metric collection when devtools panel is closed
console.log('Stopping detailed profiling...');
}
getMetrics() {
return [...this.metrics];
}
getTabData(tabId) {
if (tabId !== undefined) {
return this.tabData.get(tabId) || {
tabId,
url: '',
loadTime: 0,
resources: [],
interactions: [],
errors: []
};
}
return Array.from(this.tabData.values());
}
getPerformanceSummary() {
const allTabData = Array.from(this.tabData.values());
return {
totalTabs: allTabData.length,
averageLoadTime: allTabData.length > 0
? allTabData.reduce((sum, tab) => sum + tab.loadTime, 0) / allTabData.length
: 0,
totalResources: allTabData.reduce((sum, tab) => sum + tab.resources.length, 0),
totalInteractions: allTabData.reduce((sum, tab) => sum + tab.interactions.length, 0),
totalErrors: allTabData.reduce((sum, tab) => sum + tab.errors.length, 0),
slowestTabs: allTabData
.sort((a, b) => b.loadTime - a.loadTime)
.slice(0, 5)
.map(tab => ({
url: tab.url,
loadTime: tab.loadTime,
resourceCount: tab.resources.length
}))
};
}
async injectContentScript(tabId) {
if (chrome.scripting) {
try {
await chrome.scripting.executeScript({
target: { tabId },
files: ['content-script.js']
});
console.log(`Content script injected into tab ${tabId}`);
}
catch (error) {
console.error(`Failed to inject content script into tab ${tabId}:`, error);
}
}
}
clearMetrics() {
this.metrics = [];
this.tabData.clear();
}
destroy() {
this.isActive = false;
this.clearMetrics();
console.log('StackSleuth Browser Extension destroyed');
}
}
exports.BrowserExtension = BrowserExtension;
// Export for use in different extension contexts
exports.default = BrowserExtension;
//# sourceMappingURL=index.js.map