Best Practices
Guidelines for building reliable and efficient browser automation with Browserman.
Account Management
Use Descriptive Names
Choose clear, meaningful names for your accounts:
Good:
personal-twitter
work-twitter-official
xueqiu-trading-main
eastmoney-analysisBad:
account1
test
a
twitter123Organize Accounts
Use a consistent naming convention:
{purpose}-{platform}-{identifier}
Examples:
personal-twitter-main
work-xueqiu-primary
brand-twitter-official
testing-twitter-devSeparate Environments
Use different accounts for different environments:
const ACCOUNTS = {
development: 'dev-twitter',
staging: 'staging-twitter',
production: 'prod-twitter'
};
const accountName = ACCOUNTS[process.env.NODE_ENV];Monitor Account Health
Regularly check account status:
async function checkAccountHealth() {
const accounts = await client.accounts.list();
const unhealthy = accounts.filter(acc => acc.status !== 'active');
if (unhealthy.length > 0) {
console.warn('Unhealthy accounts:', unhealthy);
// Send alert
await notifyTeam(unhealthy);
}
}
// Run daily
cron.schedule('0 0 * * *', checkAccountHealth);Rate Limiting
Respect Platform Limits
Each platform has different rate limits:
const RATE_LIMITS = {
twitter: {
tweets: { count: 300, window: 3 * 60 * 60 * 1000 }, // 3 hours
likes: { count: 1000, window: 24 * 60 * 60 * 1000 } // 24 hours
},
xueqiu: {
posts: { count: 50, window: 24 * 60 * 60 * 1000 }
}
};Implement Request Throttling
Use a queue to control request rate:
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
async acquire() {
const now = Date.now();
// Remove old requests outside window
this.requests = this.requests.filter(
time => now - time < this.windowMs
);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest);
await sleep(waitTime);
return this.acquire();
}
this.requests.push(now);
}
}
// Usage
const limiter = new RateLimiter(300, 3 * 60 * 60 * 1000);
for (const tweet of tweets) {
await limiter.acquire();
await postTweet(tweet);
}Add Delays Between Requests
Mimic human behavior with random delays:
async function randomDelay(min = 1000, max = 5000) {
const delay = Math.random() * (max - min) + min;
await sleep(delay);
}
async function postMultipleTweets(tweets) {
for (const tweet of tweets) {
await postTweet(tweet);
await randomDelay(2000, 5000); // 2-5 seconds
}
}Error Handling
Implement Retry Logic
Handle transient errors with exponential backoff:
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
const isLastAttempt = i === maxRetries - 1;
if (isLastAttempt) {
throw error;
}
// Check if error is retryable
if (!isRetryableError(error)) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s, 8s...
const waitTime = Math.pow(2, i) * 1000;
console.log(`Retry ${i + 1}/${maxRetries} after ${waitTime}ms`);
await sleep(waitTime);
}
}
}
function isRetryableError(error) {
const retryableCodes = [
'RATE_LIMITED',
'TIMEOUT',
'PLATFORM_ERROR',
'CONNECTION_ERROR'
];
return retryableCodes.includes(error.code);
}
// Usage
const result = await retryWithBackoff(async () => {
return await client.twitter.createTweet({
accountName: 'my-twitter',
text: 'Hello World!'
});
});Handle Specific Errors
Different errors require different handling:
async function executeTaskWithHandling(task) {
try {
return await client.tasks.create(task);
} catch (error) {
switch (error.code) {
case 'RATE_LIMITED':
// Wait and retry
await sleep(error.retryAfter * 1000);
return executeTaskWithHandling(task);
case 'ACCOUNT_NOT_FOUND':
// Log and skip
console.error(`Account not found: ${task.accountName}`);
return null;
case 'AUTHENTICATION_FAILED':
// Reconnect account
await reconnectAccount(task.accountName);
return executeTaskWithHandling(task);
case 'INVALID_PARAMETERS':
// Log and don't retry
console.error(`Invalid parameters:`, task.parameters);
throw error;
default:
throw error;
}
}
}Log Errors Properly
Include context in error logs:
try {
await client.tasks.create(task);
} catch (error) {
console.error('Task creation failed:', {
error: error.message,
code: error.code,
task: {
platform: task.platform,
tool: task.tool,
accountName: task.accountName
},
timestamp: new Date().toISOString()
});
}Task Management
Always Check Task Status
Don't assume tasks succeeded:
async function executeAndVerify(task) {
// Create task
const created = await client.tasks.create(task);
// Wait for completion
const result = await client.tasks.waitFor(created.taskId);
// Verify success
if (result.state !== 'completed') {
throw new Error(`Task failed: ${result.error?.message}`);
}
return result.result;
}Implement Polling Wisely
Use appropriate polling intervals:
async function pollTaskStatus(taskId, maxWaitTime = 300000) {
const startTime = Date.now();
let pollInterval = 5000; // Start with 5 seconds
while (Date.now() - startTime < maxWaitTime) {
const status = await client.tasks.get(taskId);
if (['completed', 'failed', 'timeout', 'cancelled'].includes(status.state)) {
return status;
}
await sleep(pollInterval);
// Increase interval gradually
pollInterval = Math.min(pollInterval * 1.2, 30000); // Max 30s
}
throw new Error('Polling timeout');
}Use Webhooks for Long Tasks
For tasks that take a while, use webhooks instead of polling:
// Configure webhook
await client.webhooks.create({
url: 'https://your-app.com/webhooks/browserman',
events: ['task.completed', 'task.failed']
});
// Create task without waiting
const task = await client.tasks.create({...});
// Your webhook handler will be called when complete
app.post('/webhooks/browserman', (req, res) => {
const { event, taskId, data } = req.body;
if (event === 'task.completed') {
handleTaskSuccess(taskId, data.result);
} else if (event === 'task.failed') {
handleTaskFailure(taskId, data.error);
}
res.status(200).send('OK');
});Engine Selection
Choose the Right Engine
Use lite mode by default, full mode when needed:
function selectEngine(task) {
// Use full mode for:
// - Important operations
// - When lite mode gets detected
// - Sensitive accounts
const useFullMode =
task.priority === 'high' ||
task.account.hasDetectionIssues ||
task.operation === 'sensitive';
return useFullMode ? 'full' : 'lite';
}
await client.tasks.create({
platform: 'twitter',
tool: 'createTweet',
accountName: 'important-account',
parameters: {
text: 'Important announcement',
preferredEngine: selectEngine(task)
}
});Monitor Engine Performance
Track which engine works best for your use case:
const engineStats = {
lite: { success: 0, failure: 0 },
full: { success: 0, failure: 0 }
};
async function executeWithTracking(task) {
const engine = task.parameters.preferredEngine || 'lite';
const startTime = Date.now();
try {
const result = await client.tasks.create(task);
engineStats[engine].success++;
console.log(`${engine} mode succeeded in ${Date.now() - startTime}ms`);
return result;
} catch (error) {
engineStats[engine].failure++;
console.error(`${engine} mode failed:`, error.message);
// Try full mode if lite failed
if (engine === 'lite') {
console.log('Retrying with full mode...');
task.parameters.preferredEngine = 'full';
return executeWithTracking(task);
}
throw error;
}
}Security
Never Expose API Keys
// ❌ Bad
const API_KEY = 'sk_live_abc123';
// ✅ Good
const API_KEY = process.env.BROWSERMAN_API_KEY;
if (!API_KEY) {
throw new Error('BROWSERMAN_API_KEY not set');
}Validate Input
Always validate user input before creating tasks:
function validateTweet(text) {
if (!text || typeof text !== 'string') {
throw new Error('Tweet text is required');
}
if (text.length > 280) {
throw new Error('Tweet too long (max 280 characters)');
}
if (text.length === 0) {
throw new Error('Tweet cannot be empty');
}
return text.trim();
}
const tweet = validateTweet(userInput);
await postTweet(tweet);Sanitize Content
Remove sensitive information before posting:
function sanitizeContent(text) {
// Remove email addresses
text = text.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[email]');
// Remove phone numbers
text = text.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[phone]');
// Remove API keys
text = text.replace(/sk_[a-zA-Z0-9]{32,}/g, '[redacted]');
return text;
}Performance
Batch Operations
Process multiple tasks efficiently:
async function batchPostTweets(tweets, accountName) {
const taskPromises = tweets.map(async (tweet, index) => {
// Stagger requests to avoid rate limiting
await sleep(index * 2000);
return client.tasks.create({
platform: 'twitter',
tool: 'createTweet',
accountName,
parameters: { text: tweet }
});
});
// Wait for all tasks
const results = await Promise.allSettled(taskPromises);
// Check results
const succeeded = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`Posted ${succeeded.length}/${tweets.length} tweets`);
if (failed.length > 0) {
console.error(`Failed to post ${failed.length} tweets`);
}
return { succeeded, failed };
}Cache Platform Schema
Cache the platform schema to avoid repeated requests:
let platformSchemaCache = null;
let cacheTime = null;
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
async function getPlatformSchema() {
const now = Date.now();
if (platformSchemaCache && cacheTime && now - cacheTime < CACHE_TTL) {
return platformSchemaCache;
}
platformSchemaCache = await client.platforms.getSchema();
cacheTime = now;
return platformSchemaCache;
}Monitor Performance
Track and log performance metrics:
class PerformanceMonitor {
constructor() {
this.metrics = {
taskCount: 0,
successCount: 0,
failureCount: 0,
totalDuration: 0
};
}
async trackTask(fn) {
const startTime = Date.now();
this.metrics.taskCount++;
try {
const result = await fn();
this.metrics.successCount++;
return result;
} catch (error) {
this.metrics.failureCount++;
throw error;
} finally {
this.metrics.totalDuration += Date.now() - startTime;
}
}
getStats() {
return {
...this.metrics,
successRate: this.metrics.successCount / this.metrics.taskCount,
avgDuration: this.metrics.totalDuration / this.metrics.taskCount
};
}
}
const monitor = new PerformanceMonitor();
// Track tasks
await monitor.trackTask(() => postTweet('Hello!'));
// View stats
console.log(monitor.getStats());Testing
Test in Development
Always test with development accounts first:
const isDev = process.env.NODE_ENV === 'development';
const accountName = isDev ? 'dev-twitter' : 'prod-twitter';
await client.tasks.create({
platform: 'twitter',
tool: 'createTweet',
accountName,
parameters: {
text: isDev ? '[TEST] Hello World' : 'Hello World'
}
});Mock for Unit Tests
Mock the Browserman client in tests:
// __mocks__/browserman-client.js
export class BrowsermanClient {
async tasks.create(task) {
return {
taskId: 'mock_task_123',
state: 'pending'
};
}
async tasks.get(taskId) {
return {
taskId,
state: 'completed',
result: { success: true }
};
}
}