Skip to main content

Custom BrowserTest Wrapper

Create a custom wrapper class for enhanced functionality:
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

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:
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

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

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

// 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

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'
  }
});