Skip to main content
Effective device management is crucial for scaling your mobile automation. Whether you’re testing on a single device or orchestrating dozens simultaneously, understanding device management patterns will help you build robust automation workflows.

Understanding devices

In Uplink, a device (also called a worker) is a mobile phone or tablet that connects to your session and can host browsers. Devices can join sessions through either:
  • Connect app: Users scan a QR code or enter a session code
  • SDK integration: Your custom app connects programmatically

Connecting devices

Waiting for devices

The most common pattern is to wait for a device to connect before starting automation:
const client = await uplink.client.connect('wss://relay.uplink.build/session/<jwt>')

// Wait for any device to connect
const device = await client.worker()
console.log('Device connected:', device.address)

// Now you can use the device
const browser = await device.launch()

Listing connected devices

Get all currently connected devices:
const devices = client.workers()

console.log(`${devices.length} devices connected`)
devices.forEach(device => {
  console.log('Device:', device.address)
})

Device connection events

React to devices connecting and disconnecting in real-time:
client.on('worker-connected', async (device) => {
  console.log('New device connected:', device.address)

  // Automatically start automation on new devices
  const browser = await device.launch()
  const page = await browser.newPage()
  await page.goto('https://example.com')
})

client.on('worker-disconnected', (device) => {
  console.log('Device disconnected:', device.address)
})

Device information

Retrieve information about a device’s capabilities and specifications:
const info = await device.getDeviceInfo()

console.log('Device model:', info.deviceModel)      // e.g., "iPhone 14 Pro"
console.log('Platform:', info.platform)             // "iOS" or "Android"
console.log('OS version:', info.platformVersion)    // e.g., "17.2"
console.log('Type:', info.deviceType)              // "phone" or "tablet"

Using device info for targeting

const devices = client.workers()

// Find iOS devices
const iosDevices = []
for (const device of devices) {
  const info = await device.getDeviceInfo()
  if (info.platform === 'iOS') {
    iosDevices.push(device)
  }
}

// Test on a specific device type
const iphone = devices.find(async d => {
  const info = await d.getDeviceInfo()
  return info.deviceModel.includes('iPhone')
})

Multi-device patterns

Parallel execution

Run automation on multiple devices simultaneously:
const client = await uplink.client.connect('wss://relay.uplink.build/session/<jwt>')

// Wait for multiple devices
const device1 = await client.worker()
const device2 = await client.worker()
const device3 = await client.worker()

// Run tests in parallel
await Promise.all([
  runTest(device1),
  runTest(device2),
  runTest(device3)
])

async function runTest(device) {
  const browser = await device.launch()
  const page = await browser.newPage()
  await page.goto('https://example.com')
  // ... test logic
  await browser.close()
}

Device pool management

Manage a pool of devices for load distribution:
class DevicePool {
  constructor(client) {
    this.client = client
    this.available = []
    this.busy = new Set()

    client.on('worker-connected', (device) => {
      this.available.push(device)
    })
  }

  async acquire() {
    // Wait for an available device
    while (this.available.length === 0) {
      await new Promise(resolve => setTimeout(resolve, 100))
    }

    const device = this.available.shift()
    this.busy.add(device)
    return device
  }

  release(device) {
    this.busy.delete(device)
    this.available.push(device)
  }
}

// Usage
const pool = new DevicePool(client)

async function runAutomation() {
  const device = await pool.acquire()
  try {
    const browser = await device.launch()
    // ... automation logic
    await browser.close()
  } finally {
    pool.release(device)
  }
}

// Run multiple automations
await Promise.all([
  runAutomation(),
  runAutomation(),
  runAutomation()
])

Platform-specific testing

Run different tests based on platform:
client.on('worker-connected', async (device) => {
  const info = await device.getDeviceInfo()

  if (info.platform === 'iOS') {
    await runIOSTest(device)
  } else if (info.platform === 'Android') {
    await runAndroidTest(device)
  }
})

async function runIOSTest(device) {
  const browser = await device.launch()
  // iOS-specific test logic
  await browser.close()
}

async function runAndroidTest(device) {
  const browser = await device.launch()
  // Android-specific test logic
  await browser.close()
}

Browser management per device

Each device can host multiple browsers. Manage them effectively:

Launching browsers

// Launch a new browser on a specific device
const browser = await device.launch()

// Or let the client pick a device
const browser = await client.launch()

Listing browsers

// Get all browsers on a device
const browsers = await device.browsers()

console.log(`${browsers.length} browsers running on device`)

Connecting to existing browsers

Browsers are persistent and can be reconnected using their handle:
// Save the browser handle
const browser = await device.launch()
const handle = browser.handle

// Later, reconnect to the same browser
const sameBrowser = await device.connect(handle)

// Or from the client level
const sameBrowser = await client.connect(handle)

Cleaning up browsers

Always close browsers when done to free device resources:
// Close all browsers on a device
const browsers = await device.browsers()
await Promise.all(browsers.map(b => b.close()))

Device lifecycle

Terminating devices

When necessary, you can terminate a device connection:
// Terminate a specific device
await device.terminate()

// Or from the client level
await client.terminate(device.address)
Terminating a device closes all browsers and disconnects the device from the session. Only do this when you’re certain you’re done with the device.

Handling disconnections

Devices can disconnect unexpectedly (network issues, app closure, etc.). Handle disconnections gracefully:
const activeDevices = new Map()

client.on('worker-connected', (device) => {
  activeDevices.set(device.address, device)
})

client.on('worker-disconnected', (device) => {
  console.warn('Device disconnected:', device.address)
  activeDevices.delete(device.address)

  // Implement reconnection logic or failover
  handleDeviceFailure(device.address)
})

async function handleDeviceFailure(address) {
  // Retry logic, notification, or reassignment
}

Addressing devices

Devices have unique addresses for identification and targeting:
const devices = client.workers()

devices.forEach(device => {
  console.log('Address:', device.address)
})

// Use a specific device by address
const targetDevice = devices.find(d => d.address === 'device-123')
if (targetDevice) {
  const browser = await targetDevice.launch()
}

Best practices

Always close browsers and pages when done to free device resources:
try {
  const browser = await device.launch()
  const page = await browser.newPage()
  // ... automation
} finally {
  await page.close()
  await browser.close()
}
Different devices have different capabilities. Test your automation across various device types and OS versions.
const info = await device.getDeviceInfo()
if (parseFloat(info.platformVersion) < 15) {
  console.warn('Old OS version, some features may not work')
}
Keep track of device connection stability and browser success rates:
const stats = {
  connected: 0,
  disconnected: 0,
  failures: 0
}

client.on('worker-connected', () => stats.connected++)
client.on('worker-disconnected', () => stats.disconnected++)
Set appropriate timeouts when waiting for devices or operations:
const timeout = 30000 // 30 seconds
const device = await Promise.race([
  client.worker(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Device connection timeout')), timeout)
  )
])

Next steps