Custom BrowserTest Wrapper
Create a custom wrapper class for enhanced functionality:Copy
class AdvancedBrowserTest {
constructor(config) {
this.bt = new BrowserTest(config);
this.cache = new Map();
this.metrics = {
operations: 0,
errors: 0,
totalTime: 0
};
}
async screenshot(url, options = {}) {
const startTime = Date.now();
this.metrics.operations++;
try {
// Check cache first
const cacheKey = `${url}:${JSON.stringify(options)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const result = await this.bt.screenshot.take({ url, ...options });
// Cache successful results for 5 minutes
if (result.success) {
this.cache.set(cacheKey, result);
setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000);
}
this.metrics.totalTime += Date.now() - startTime;
return result;
} catch (error) {
this.metrics.errors++;
throw error;
}
}
async test(instructions, url, options = {}) {
const startTime = Date.now();
this.metrics.operations++;
try {
const result = await this.bt.testing.execute({
instructions,
url,
...options
});
this.metrics.totalTime += Date.now() - startTime;
return result;
} catch (error) {
this.metrics.errors++;
throw error;
}
}
getMetrics() {
return {
...this.metrics,
averageTime: this.metrics.operations > 0 ?
this.metrics.totalTime / this.metrics.operations : 0,
errorRate: this.metrics.operations > 0 ?
(this.metrics.errors / this.metrics.operations) * 100 : 0
};
}
clearCache() {
this.cache.clear();
}
}
// Usage
const abt = new AdvancedBrowserTest({
apiKey: process.env.BROWSERTEST_API_KEY
});
// Use with caching
const screenshot1 = await abt.screenshot('https://example.com');
const screenshot2 = await abt.screenshot('https://example.com'); // From cache
console.log('Metrics:', abt.getMetrics());
Parallel Processing with Rate Limiting
Copy
class RateLimitedProcessor {
constructor(bt, requestsPerMinute = 60) {
this.bt = bt;
this.requestsPerMinute = requestsPerMinute;
this.requests = [];
this.lastRequestTime = 0;
this.interval = 60000 / requestsPerMinute; // ms between requests
}
async addRequest(request) {
return new Promise((resolve, reject) => {
this.requests.push({ request, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.requests.length === 0) return;
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest >= this.interval) {
const { request, resolve, reject } = this.requests.shift();
try {
const result = await this.executeRequest(request);
resolve(result);
} catch (error) {
reject(error);
}
this.lastRequestTime = now;
} else {
// Wait for the next available slot
setTimeout(() => this.processQueue(), this.interval - timeSinceLastRequest);
}
}
async executeRequest(request) {
// Determine request type and execute accordingly
if (request.url && !request.instructions) {
return this.bt.screenshot.take(request);
} else if (request.instructions && request.url) {
return this.bt.testing.execute(request);
} else {
throw new Error('Invalid request format');
}
}
async processBatch(requests) {
const promises = requests.map(request => this.addRequest(request));
return Promise.all(promises);
}
}
// Usage
const processor = new RateLimitedProcessor(bt, 30); // 30 requests per minute
const results = await processor.processBatch([
{ url: 'https://site1.com', fullPage: true },
{ url: 'https://site2.com', fullPage: true },
{ instructions: 'Test login', url: 'https://app.com/login' }
]);
Progressive Enhancement
Handle different API availability levels:Copy
class ProgressiveBrowserTest {
constructor(config) {
this.config = config;
this.features = {};
this.testFeatures();
}
async testFeatures() {
try {
await this.bt.testConnection();
this.features.connection = true;
} catch {
this.features.connection = false;
}
try {
await this.bt.getUsage();
this.features.usage = true;
} catch {
this.features.usage = false;
}
// Test screenshot capability
try {
await this.bt.screenshot.take({
url: 'https://httpbin.org/html',
timeout: 5000
});
this.features.screenshot = true;
} catch {
this.features.screenshot = false;
}
// Test testing capability
try {
await this.bt.testing.execute({
instructions: 'Navigate to page',
url: 'https://httpbin.org/html',
config: { timeout: 5000 }
});
this.features.testing = true;
} catch {
this.features.testing = false;
}
}
async screenshot(url, options = {}) {
if (!this.features.connection) {
throw new Error('BrowserTest API unavailable');
}
if (!this.features.screenshot) {
// Fallback to basic HTTP request for title extraction
return this.fallbackScreenshot(url);
}
return this.bt.screenshot.take({ url, ...options });
}
async test(instructions, url, options = {}) {
if (!this.features.connection) {
throw new Error('BrowserTest API unavailable');
}
if (!this.features.testing) {
// Fallback to basic connectivity test
return this.fallbackTest(url);
}
return this.bt.testing.execute({ instructions, url, ...options });
}
async fallbackScreenshot(url) {
// Basic fallback - just get page title via fetch
try {
const response = await fetch(url);
const html = await response.text();
const title = html.match(/<title>(.*?)<\/title>/)?.[1] || 'Unknown';
return {
success: true,
data: {
screenshot: '', // No screenshot
url,
title,
width: 0,
height: 0,
format: '',
fullPage: false
},
meta: {
timeMs: 0,
size: 0
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async fallbackTest(url) {
// Basic connectivity test
try {
const response = await fetch(url, { method: 'HEAD' });
return {
success: response.ok,
results: {
actions: [],
output: `Page responded with status ${response.status}`
},
meta: {
timeMs: 0,
url
}
};
} catch (error) {
return {
success: false,
results: {
actions: [],
output: `Connection failed: ${error.message}`
},
meta: {
timeMs: 0,
url
}
};
}
}
getFeatures() {
return { ...this.features };
}
}
// Usage
const pbt = new ProgressiveBrowserTest({
apiKey: process.env.BROWSERTEST_API_KEY
});
// Wait for feature detection
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Available features:', pbt.getFeatures());
// Use with automatic fallbacks
const result = await pbt.screenshot('https://example.com');
Custom Result Processing
Copy
class ResultProcessor {
constructor(bt) {
this.bt = bt;
this.processors = new Map();
}
registerProcessor(type, processor) {
this.processors.set(type, processor);
}
async processScreenshot(url, options = {}, processingOptions = {}) {
const result = await this.bt.screenshot.take({ url, ...options });
if (!result.success) {
return result;
}
const processor = this.processors.get('screenshot');
if (processor) {
return processor(result, processingOptions);
}
return result;
}
async processTest(instructions, url, options = {}, processingOptions = {}) {
const result = await this.bt.testing.execute({
instructions,
url,
...options
});
if (!result.success) {
return result;
}
const processor = this.processors.get('test');
if (processor) {
return processor(result, processingOptions);
}
return result;
}
}
// Register processors
const processor = new ResultProcessor(bt);
// Image processing for screenshots
processor.registerProcessor('screenshot', async (result, options) => {
const imageBuffer = Buffer.from(result.data.screenshot, 'base64');
// Apply processing based on options
if (options.resize) {
// Resize image (would use sharp or similar)
result.data.processed = true;
}
if (options.compress) {
// Compress image
result.data.compressed = true;
}
return result;
});
// Data extraction for tests
processor.registerProcessor('test', async (result, options) => {
if (options.extractPatterns) {
const patterns = options.extractPatterns;
const extracted = {};
for (const [key, pattern] of Object.entries(patterns)) {
const match = result.results.output.match(new RegExp(pattern));
if (match) {
extracted[key] = match[1] || match[0];
}
}
result.results.extracted = extracted;
}
return result;
});
// Usage
const processedScreenshot = await processor.processScreenshot(
'https://example.com',
{ fullPage: true },
{ resize: { width: 800 }, compress: true }
);
const processedTest = await processor.processTest(
'Extract user information',
'https://example.com/profile',
{},
{
extractPatterns: {
email: 'Email: ([^\\s]+)',
name: 'Name: ([^\\n]+)'
}
}
);
Monitoring and Analytics
Copy
class BrowserTestAnalytics {
constructor(bt) {
this.bt = bt;
this.metrics = {
operations: [],
errors: [],
performance: {
averageResponseTime: 0,
totalRequests: 0,
errorRate: 0
}
};
}
async trackOperation(operation, params, startTime) {
const duration = Date.now() - startTime;
this.metrics.operations.push({
operation,
params,
duration,
timestamp: new Date().toISOString()
});
this.updatePerformanceMetrics(duration);
}
trackError(operation, error, params) {
this.metrics.errors.push({
operation,
error: error.message,
params,
timestamp: new Date().toISOString()
});
this.metrics.performance.errorRate =
this.metrics.errors.length / this.metrics.operations.length;
}
updatePerformanceMetrics(duration) {
this.metrics.performance.totalRequests++;
const totalTime = this.metrics.operations.reduce((sum, op) => sum + op.duration, 0);
this.metrics.performance.averageResponseTime = totalTime / this.metrics.operations.length;
}
async executeWithTracking(operation, ...args) {
const operationName = operation.name || 'anonymous';
const startTime = Date.now();
try {
const result = await operation.apply(this.bt, args);
await this.trackOperation(operationName, args, startTime);
return result;
} catch (error) {
this.trackError(operationName, error, args);
throw error;
}
}
getMetrics() {
return {
...this.metrics,
summary: {
totalOperations: this.metrics.operations.length,
totalErrors: this.metrics.errors.length,
uptime: this.calculateUptime(),
topErrors: this.getTopErrors()
}
};
}
calculateUptime() {
if (this.metrics.operations.length === 0) return 0;
const total = this.metrics.operations.length;
const errors = this.metrics.errors.length;
return ((total - errors) / total) * 100;
}
getTopErrors() {
const errorCounts = {};
this.metrics.errors.forEach(error => {
errorCounts[error.error] = (errorCounts[error.error] || 0) + 1;
});
return Object.entries(errorCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([error, count]) => ({ error, count }));
}
generateReport() {
const metrics = this.getMetrics();
return {
timestamp: new Date().toISOString(),
period: 'current_session',
metrics,
recommendations: this.generateRecommendations(metrics)
};
}
generateRecommendations(metrics) {
const recommendations = [];
if (metrics.performance.errorRate > 0.1) {
recommendations.push('High error rate detected. Check API key and network connectivity.');
}
if (metrics.performance.averageResponseTime > 10000) {
recommendations.push('Slow response times detected. Consider using batch operations.');
}
const topErrors = metrics.summary.topErrors;
if (topErrors.length > 0) {
recommendations.push(`Most common error: "${topErrors[0].error}" (${topErrors[0].count} occurrences)`);
}
return recommendations;
}
}
// Usage
const analytics = new BrowserTestAnalytics(bt);
// Wrap operations for tracking
const screenshot = await analytics.executeWithTracking(
bt.screenshot.take,
{ url: 'https://example.com' }
);
// Get analytics
const report = analytics.generateReport();
console.log('Analytics Report:', report);
Integration Patterns
Express.js Middleware
Copy
// browserTestMiddleware.js
const BrowserTest = require('browsertest-sdk');
function createBrowserTestMiddleware(config) {
const bt = new BrowserTest(config);
return {
// Screenshot endpoint
async screenshot(req, res) {
try {
const { url, ...options } = req.body;
const result = await bt.screenshot.take({ url, ...options });
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Test endpoint
async test(req, res) {
try {
const { instructions, url, ...options } = req.body;
const result = await bt.testing.execute({ instructions, url, ...options });
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Usage endpoint
async usage(req, res) {
try {
const usage = await bt.getUsage();
res.json(usage);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
};
}
module.exports = createBrowserTestMiddleware;
// Usage in Express app
const express = require('express');
const browserTestMiddleware = require('./browserTestMiddleware');
const app = express();
app.use(express.json());
const bt = browserTestMiddleware({
apiKey: process.env.BROWSERTEST_API_KEY
});
app.post('/api/screenshot', bt.screenshot);
app.post('/api/test', bt.test);
app.get('/api/usage', bt.usage);
app.listen(3000, () => {
console.log('BrowserTest API server running on port 3000');
});
Queue-Based Processing
Copy
const { EventEmitter } = require('events');
class BrowserTestQueue extends EventEmitter {
constructor(bt, options = {}) {
super();
this.bt = bt;
this.queue = [];
this.processing = false;
this.concurrency = options.concurrency || 2;
this.processingCount = 0;
}
add(job) {
this.queue.push(job);
this.emit('job_added', job);
this.process();
}
async process() {
if (this.processing || this.processingCount >= this.concurrency || this.queue.length === 0) {
return;
}
this.processing = true;
const job = this.queue.shift();
this.processingCount++;
try {
this.emit('job_started', job);
const result = await this.executeJob(job);
this.emit('job_completed', job, result);
} catch (error) {
this.emit('job_failed', job, error);
} finally {
this.processingCount--;
this.processing = false;
this.process(); // Process next job
}
}
async executeJob(job) {
switch (job.type) {
case 'screenshot':
return this.bt.screenshot.take(job.options);
case 'test':
return this.bt.testing.execute(job.options);
case 'template':
return this.bt.template.invoke(job.templateId, job.options);
default:
throw new Error(`Unknown job type: ${job.type}`);
}
}
getStats() {
return {
queueLength: this.queue.length,
processingCount: this.processingCount,
totalProcessed: this.totalProcessed || 0
};
}
}
// Usage
const queue = new BrowserTestQueue(bt, { concurrency: 3 });
queue.on('job_completed', (job, result) => {
console.log(`Job ${job.id} completed`);
});
queue.on('job_failed', (job, error) => {
console.error(`Job ${job.id} failed:`, error.message);
});
// Add jobs
queue.add({
id: 'screenshot-1',
type: 'screenshot',
options: { url: 'https://example.com', fullPage: true }
});
queue.add({
id: 'test-1',
type: 'test',
options: {
instructions: 'Test homepage',
url: 'https://example.com'
}
});
