# @testrelic/appium-analytics

[![npm version](https://img.shields.io/npm/v/@testrelic/appium-analytics.svg)](https://www.npmjs.com/package/@testrelic/appium-analytics)
[![license](https://img.shields.io/npm/l/@testrelic/appium-analytics.svg)](https://github.com/testrelic-ai/testrelic-sdk/blob/main/packages/appium-analytics/LICENSE)
[![node](https://img.shields.io/node/v/@testrelic/appium-analytics.svg)](https://nodejs.org)

Appium test analytics reporter with device log capture, command tracking, network interception, and interactive HTML reports for Android and iOS.

---

## Table of Contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [WDIO Integration](#wdio-integration)
  - [TestRelicService](#testrelicservice)
  - [TestRelicReporter](#testrelicreporter)
  - [onComplete Hook](#oncomplete-hook)
  - [Android Configuration](#android-configuration)
  - [iOS Configuration](#ios-configuration)
- [Configuration Reference](#configuration-reference)
  - [Core Options](#core-options)
  - [Cloud Options](#cloud-options)
- [Platform Setup](#platform-setup)
  - [Android](#android)
  - [iOS](#ios)
- [Cloud Upload](#cloud-upload)
  - [Config File](#config-file)
  - [Environment Variables](#environment-variables)
  - [Priority Order](#priority-order)
- [CLI](#cli)
- [Merging Reports](#merging-reports)
- [Viewing Reports Locally](#viewing-reports-locally)
- [Redaction](#redaction)
- [Troubleshooting](#troubleshooting)
- [License](#license)

---

## Features

- **Appium command tracking** — records every WebDriver command with timing, category, arguments, and return values; sensitive arguments are automatically redacted
- **Device log capture** — streams Android logcat and iOS syslog / crashlog per test
- **Network interception** — captures HTTP requests and responses via Chrome DevTools Protocol (Android), `safariNetwork` (iOS), or an automatic proxy fallback using `adb` when CDP is not available
- **Console log capture** — collects webview / browser console output via BiDi `log.entryAdded` or falls back to `browser` / `safariConsole` log polling
- **Screenshot collection** — captures screenshots after every test, only on failure, or never
- **Video recording** — records the device screen using Appium recording commands or `adb screenrecord` on Android
- **Interactive HTML reports** — self-contained report with a timeline view for commands, logs, network requests, screenshots, and video
- **Cloud upload** — upload results to the TestRelic platform in batch or realtime mode
- **CLI** — merge multiple report files, serve the report directory, or open the report in a browser

---

## Requirements

| Requirement | Version |
|-------------|---------|
| Node.js | >= 18 (Appium 3.x requires >= 20.19) |
| WebdriverIO | >= 9.0.0 *(peer dependency)* |
| `@wdio/reporter` | >= 9.0.0 *(peer dependency)* |
| Appium | 3.x (`npm install -g appium@latest`) |
| Android driver | UiAutomator2 (`appium driver install uiautomator2`) |
| iOS driver | XCUITest on macOS (`appium driver install xcuitest`) |

> **iOS note:** iOS testing requires macOS 13+ with Xcode 15+. iOS tests cannot run on Windows or Linux.

---

## Installation

```bash
# npm
npm install @testrelic/appium-analytics
npm install --save-dev webdriverio @wdio/reporter

# yarn
yarn add @testrelic/appium-analytics
yarn add --dev webdriverio @wdio/reporter

# pnpm
pnpm add @testrelic/appium-analytics
pnpm add --save-dev webdriverio @wdio/reporter
```

---

## Quick Start

Add `TestRelicService` to `services` and `TestRelicReporter` to `reporters` in your WDIO config, then implement `onComplete`:

```ts
// wdio.conf.ts
import { join } from 'node:path';
import { TestRelicService } from '@testrelic/appium-analytics/service';
import { TestRelicReporter } from '@testrelic/appium-analytics';

const REPORT_DIR = join(process.cwd(), 'test-results');

async function onComplete() {
  try {
    await TestRelicReporter.finalize(process.cwd());
  } catch (err) {
    process.stderr.write(`[TestRelic] Cloud finalization failed: ${err}\n`);
  }
}

export const config = {
  runner: 'local',
  port: 4723,
  path: '/',

  specs: ['./tests/**/*.test.ts'],
  maxInstances: 1,

  capabilities: [{
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:deviceName': 'emulator-5554',
    'appium:app': join(process.cwd(), 'apps', 'MyApp.apk'),
  }],

  framework: 'mocha',
  mochaOpts: { ui: 'bdd', timeout: 60000 },

  services: [
    [TestRelicService, {
      outputPath: REPORT_DIR,
      includeDeviceLogs: true,
      includeNetworkLogs: true,
      screenshotOnEvery: 'failure',
    }],
  ],

  reporters: [
    'spec',
    [TestRelicReporter, {
      outputPath: join(REPORT_DIR, 'report.json'),
      htmlReportPath: join(REPORT_DIR, 'report.html'),
      openReport: true,
    }],
  ],

  onComplete,
};
```

---

## WDIO Integration

The package provides two WebdriverIO plugins that work together: a **service** and a **reporter**. Both accept the same `AppiumReporterConfig` options.

### TestRelicService

Import path: `@testrelic/appium-analytics/service`

`TestRelicService` is a WDIO service plugin that runs inside the **worker process** alongside your live Appium/WebDriver session. It registers the following hooks:

| Hook | What it does |
|------|-------------|
| `before` | Detects platform/capabilities, starts `DeviceLogCollector`, `ConsoleLogCollector`, `NetworkInterceptor`, and prepares `ScreenshotCollector` |
| `after` | Stops log and network collectors |
| `beforeTest` / `afterTest` | Marks per-test log slice boundaries; captures screenshots; starts/stops video recording |
| `beforeCommand` / `afterCommand` | Delegates to `CommandTracker` — records every Appium command with timing and redacted arguments |
| `beforeAssertion` / `afterAssertion` | Delegates to `AssertionTracker` — records WebdriverIO v9 assertion steps |

### TestRelicReporter

Import path: `@testrelic/appium-analytics`

`TestRelicReporter` extends `@wdio/reporter` and runs in the same **worker process**. It consumes data collected by `TestRelicService` and writes the final reports:

| Event | What it does |
|-------|-------------|
| `onRunnerStart` | Initializes the optional `CloudClient` |
| `onTestEnd` | Pulls collected data from `TestRelicService`, builds a `TimelineBuildInput`, and optionally performs a realtime cloud upload |
| `onRunnerEnd` | Builds the full timeline and summary, writes the JSON report, generates the HTML report, prints the console summary, and writes a cloud finalization manifest |

### onComplete Hook

> **This is required for cloud upload to work correctly.**

`TestRelicReporter.finalize()` is a static method that must be called from the WDIO **launcher process** (i.e., in `onComplete`) after all worker processes have exited. It reads the `.testrelic-finalize-<testRunId>.json` manifest written by each worker and sends the final upload payload to the cloud.

```ts
async function onComplete() {
  try {
    await TestRelicReporter.finalize(process.cwd());
  } catch (err) {
    process.stderr.write(`[TestRelic] Cloud finalization failed: ${err}\n`);
  }
}

export const config = {
  // ...
  onComplete,
};
```

Even when cloud upload is not configured, including `onComplete` is harmless — `finalize()` exits immediately if no manifest files are found.

### Android Configuration

Full example using the Android emulator:

```ts
// wdio.android.conf.ts
import { join } from 'node:path';
import { TestRelicService } from '@testrelic/appium-analytics/service';
import { TestRelicReporter } from '@testrelic/appium-analytics';

const APP_PATH = join(process.cwd(), 'apps', 'MyApp.apk');
const REPORT_DIR = join(process.cwd(), 'test-results');

async function onComplete() {
  try {
    await TestRelicReporter.finalize(process.cwd());
  } catch (err) {
    process.stderr.write(`[TestRelic] Cloud finalization failed: ${err}\n`);
  }
}

export const config = {
  runner: 'local',
  port: 4723,
  path: '/',

  specs: ['./tests/android/**/*.test.ts'],
  maxInstances: 1,

  capabilities: [{
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:deviceName': 'emulator-5554',
    'appium:app': APP_PATH,
    'appium:autoGrantPermissions': true,
    'appium:newCommandTimeout': 240,
    'appium:appWaitActivity': '*',
    'appium:noReset': false,
  }],

  logLevel: 'warn',
  waitforTimeout: 15000,
  connectionRetryTimeout: 120000,
  connectionRetryCount: 3,

  framework: 'mocha',
  mochaOpts: { ui: 'bdd', timeout: 90000 },

  services: [
    [TestRelicService, {
      outputPath: REPORT_DIR,
      includeDeviceLogs: true,
      includeNetworkLogs: true,
      includeConsoleLogs: true,
      includeCommands: true,
      includeAssertions: true,
      includeScreenshots: true,
      screenshotOnEvery: 'failure',
      includeVideoRecording: true,
    }],
  ],

  reporters: [
    'spec',
    [TestRelicReporter, {
      outputPath: join(REPORT_DIR, 'report.json'),
      htmlReportPath: join(REPORT_DIR, 'report.html'),
      openReport: false,
    }],
  ],

  onComplete,

  autoCompileOpts: {
    tsNodeOpts: { project: './tsconfig.json' },
  },
};
```

### iOS Configuration

> **Requires macOS 13+ with Xcode 15+.** iOS tests cannot run on Windows or Linux.

```ts
// wdio.ios.conf.ts
import { join } from 'node:path';
import { TestRelicService } from '@testrelic/appium-analytics/service';
import { TestRelicReporter } from '@testrelic/appium-analytics';

const APP_PATH = join(process.cwd(), 'apps', 'MyApp.app');
const REPORT_DIR = join(process.cwd(), 'test-results');

async function onComplete() {
  try {
    await TestRelicReporter.finalize(process.cwd());
  } catch (err) {
    process.stderr.write(`[TestRelic] Cloud finalization failed: ${err}\n`);
  }
}

export const config = {
  runner: 'local',
  port: 4723,
  path: '/',

  specs: ['./tests/ios/**/*.test.ts'],
  maxInstances: 1,

  capabilities: [{
    platformName: 'iOS',
    'appium:automationName': 'XCUITest',
    'appium:deviceName': 'iPhone 15',
    'appium:platformVersion': '17.5',
    'appium:app': APP_PATH,
    'appium:newCommandTimeout': 240,
    'appium:noReset': false,
  }],

  logLevel: 'warn',
  waitforTimeout: 10000,
  connectionRetryTimeout: 120000,
  connectionRetryCount: 3,

  framework: 'mocha',
  mochaOpts: { ui: 'bdd', timeout: 60000 },

  services: [
    [TestRelicService, {
      outputPath: REPORT_DIR,
      includeDeviceLogs: true,
      includeNetworkLogs: true,
      includeConsoleLogs: true,
      includeCommands: true,
      includeAssertions: true,
      includeScreenshots: true,
      screenshotOnEvery: 'failure',
      includeVideoRecording: false,
    }],
  ],

  reporters: [
    'spec',
    [TestRelicReporter, {
      outputPath: join(REPORT_DIR, 'report.json'),
      htmlReportPath: join(REPORT_DIR, 'report.html'),
      openReport: false,
    }],
  ],

  onComplete,

  autoCompileOpts: {
    tsNodeOpts: { project: './tsconfig.json' },
  },
};
```

---

## Configuration Reference

Both `TestRelicService` and `TestRelicReporter` accept `Partial<AppiumReporterConfig>`. Options set on `TestRelicService` control data collection; options set on `TestRelicReporter` control report output and cloud upload. You can share the same options object between both plugins.

### Core Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `outputPath` | `string` | `'./test-results/testrelic-timeline.json'` | File path for the JSON report. Directories are created automatically. |
| `htmlReportPath` | `string` | `outputPath` with `.html` extension | File path for the HTML report. |
| `openReport` | `boolean` | `true` | Automatically open the HTML report in the default browser after the run completes. |
| `includeDeviceLogs` | `boolean` | `true` | Capture Android logcat / iOS syslog and crashlog entries. |
| `includeNetworkLogs` | `boolean` | `true` | Capture HTTP network requests and responses. |
| `includeConsoleLogs` | `boolean` | `true` | Capture webview and browser console output. |
| `includeScreenshots` | `boolean` | `true` | Capture device screenshots. |
| `screenshotOnEvery` | `'test' \| 'failure' \| 'never'` | `'failure'` | When to take screenshots: after every test, only on test failure, or never. |
| `includeVideoRecording` | `boolean` | `false` | Record the device screen for each test. |
| `includeCommands` | `boolean` | `true` | Record every Appium / WebDriver command with timing and arguments. |
| `includeAssertions` | `boolean` | `true` | Record WebdriverIO assertion steps (WDIO v9+). |
| `deviceLogPollInterval` | `number` (ms) | `1000` | Interval between device log polls. Minimum value is `100`. |
| `preferBiDi` | `boolean` | `true` | Use the WebDriver BiDi `log.entryAdded` event for console log capture when the driver supports it, falling back to polling otherwise. |
| `redactPatterns` | `(string \| RegExp)[]` | `[]` | Additional patterns to redact from command arguments and log entries. Built-in patterns are always applied (see [Redaction](#redaction)). |
| `quiet` | `boolean` | `false` | Suppress the console summary table printed at the end of a run. |
| `metadata` | `Record<string, unknown>` | `null` | Arbitrary key-value pairs attached to the test run report. Useful for build numbers, branch names, or environment tags. |
| `cloud` | `CloudReporterOptions` | — | Cloud upload configuration. See [Cloud Options](#cloud-options) below. |

### Cloud Options

Passed as the `cloud` key inside the reporter options, or loaded automatically from `.testrelic/testrelic-config.json` and environment variables.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | — | TestRelic API key. Required for cloud upload. |
| `endpoint` | `string` | `'https://platform.testrelic.ai/api/v1'` | TestRelic API endpoint. Override for self-hosted or staging environments. |
| `upload` | `'batch' \| 'realtime' \| 'both'` | `'batch'` | Upload strategy. `'batch'` uploads after `onComplete`; `'realtime'` uploads results as each test finishes; `'both'` does both. |
| `timeout` | `number` (ms) | — | HTTP request timeout for cloud API calls. |
| `projectName` | `string` | — | Override the project name shown in the TestRelic dashboard. Defaults to the git repository name. |
| `uploadArtifacts` | `boolean` | — | Whether to upload screenshots and video files as run artifacts. |
| `artifactMaxSizeMb` | `number` | — | Maximum size in MB for a single artifact file to be uploaded. Files exceeding this limit are skipped. |

---

## Platform Setup

### Android

**Prerequisites**

- Node.js >= 20.19
- Java JDK 17+
- Windows 10/11, macOS, or Linux with hardware acceleration enabled for the emulator

**Step 1 — Install Android SDK Command-Line Tools**

Download from [developer.android.com/studio#command-line-tools-only](https://developer.android.com/studio#command-line-tools-only), extract to a directory (e.g. `C:\Android\cmdline-tools\latest\`), and set the following environment variables:

```powershell
# Windows (PowerShell)
[System.Environment]::SetEnvironmentVariable("ANDROID_HOME", "C:\Android", "User")
[System.Environment]::SetEnvironmentVariable(
  "Path",
  "$env:Path;C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator",
  "User"
)
```

```bash
# macOS / Linux (~/.zshrc or ~/.bashrc)
export ANDROID_HOME=$HOME/Android
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator
```

Accept SDK licenses:

```bash
sdkmanager --licenses
```

**Step 2 — Install Platform Tools, Emulator, and System Image**

```bash
sdkmanager "platform-tools" "emulator"
sdkmanager "platforms;android-34"
sdkmanager "system-images;android-34;google_apis;x86_64"
```

**Step 3 — Create an Android Virtual Device**

```bash
avdmanager create avd -n MyEmulator -k "system-images;android-34;google_apis;x86_64" -d "pixel_7"
```

**Step 4 — Start the Emulator**

```bash
emulator -avd MyEmulator -no-snapshot-load
```

Wait for the emulator to fully boot, then verify:

```bash
adb devices
# emulator-5554   device
```

**Step 5 — Install Appium and UiAutomator2 Driver**

```bash
npm install -g appium@latest
appium driver install uiautomator2
```

Verify:

```bash
appium --version          # 3.x.x
appium driver list --installed  # uiautomator2
```

**Step 6 — Start the Appium Server**

```bash
appium --address 127.0.0.1 --port 4723
```

For the full Android setup guide see [`docs/ANDROID_SETUP.md`](docs/ANDROID_SETUP.md).

---

### iOS

> **macOS only.** iOS testing requires macOS 13+ with Xcode 15+. These tests cannot run on Windows or Linux.

**Prerequisites**

- macOS 13+ (Ventura or later recommended)
- Xcode 15+ with iOS Simulator
- Node.js >= 20.19
- Xcode Command Line Tools: `xcode-select --install`

**Step 1 — Install Appium and XCUITest Driver**

```bash
npm install -g appium@latest
appium driver install xcuitest
```

Verify:

```bash
appium driver list --installed  # xcuitest
```

**Step 2 — Boot an iOS Simulator**

```bash
xcrun simctl boot "iPhone 15"
open -a Simulator
```

List available simulators if `iPhone 15` is not found:

```bash
xcrun simctl list devices
```

**Step 3 — Start the Appium Server**

```bash
appium --address 127.0.0.1 --port 4723
```

For the full iOS setup guide see [`docs/IOS_SETUP.md`](docs/IOS_SETUP.md).

---

## Cloud Upload

Results can be uploaded to the TestRelic cloud platform. Cloud upload is optional — reports are always written locally regardless.

### Config File

Create `.testrelic/testrelic-config.json` in your project root. The file is discovered automatically by the reporter:

```json
{
  "cloud": {
    "apiKey": "<your-api-key>",
    "endpoint": "https://platform.testrelic.ai/api/v1",
    "upload": "batch"
  }
}
```

Add this file to `.gitignore` to keep your API key out of version control.

### Environment Variables

| Variable | Description |
|----------|-------------|
| `TESTRELIC_API_KEY` | TestRelic API key |
| `TESTRELIC_CLOUD_ENDPOINT` | API endpoint URL |
| `TESTRELIC_UPLOAD_STRATEGY` | Upload strategy: `batch`, `realtime`, or `both` |

Environment variables are the recommended approach for CI/CD pipelines.

### Priority Order

Configuration is merged in the following order — higher entries win:

1. `TESTRELIC_API_KEY` / `TESTRELIC_CLOUD_ENDPOINT` / `TESTRELIC_UPLOAD_STRATEGY` environment variables
2. `cloud` inline options inside the `TestRelicReporter` options block in `wdio.conf.ts`
3. `.testrelic/testrelic-config.json` config file

---

## CLI

The package installs a `testrelic-appium` binary automatically.

**Merge multiple report files** (useful after parallel worker runs):

```bash
npx testrelic-appium merge ./test-results/report-worker-1.json ./test-results/report-worker-2.json \
  -o ./test-results/merged-report.json
```

**Serve a report directory** (serves at `http://127.0.0.1:9323` by default):

```bash
npx testrelic-appium serve ./test-results
npx testrelic-appium serve ./test-results --port 8080
```

**Serve and open in browser** (alias for `serve` + auto-open):

```bash
npx testrelic-appium open ./test-results
```

---

## Merging Reports

When running tests across multiple WDIO workers, each worker writes its own JSON report. Use `mergeReports()` or the CLI `merge` command to combine them into a single timeline.

**Programmatic merge** (`@testrelic/appium-analytics/merge`):

```ts
import { mergeReports } from '@testrelic/appium-analytics/merge';

await mergeReports(
  ['./test-results/report-1.json', './test-results/report-2.json'],
  { output: './test-results/merged.json' }
);
```

**CLI merge** (see [CLI](#cli) section above).

---

## Viewing Reports Locally

After a test run, the HTML report is written to `htmlReportPath` (default: same location as `outputPath` with an `.html` extension) and opened automatically in your browser when `openReport: true` (the default).

To view a report at any time without running tests:

```bash
npx testrelic-appium serve ./test-results
# Open http://127.0.0.1:9323 in your browser
```

---

## Redaction

The reporter automatically redacts sensitive data from command arguments and log entries using the following built-in patterns:

| Pattern | Matches |
|---------|---------|
| AWS-style access keys | 20-character alphanumeric strings starting with `AKIA`, `ASIA`, etc. |
| Bearer tokens | Values following `Bearer ` in headers or strings |
| PEM private keys | `-----BEGIN * PRIVATE KEY-----` blocks |
| Embedded credentials | `//user:password@` in URLs |

To add custom redaction patterns, use the `redactPatterns` option:

```ts
[TestRelicService, {
  redactPatterns: [
    /my-secret-\w+/,        // regex
    'my-fixed-token-value', // literal string
  ],
}]
```

Custom patterns are appended to the built-in set — the built-in patterns are always active.

---

## Troubleshooting

### Android: emulator won't start
- Enable Intel VT-x / AMD-V in BIOS settings
- On Windows 11: ensure the Hyper-V platform feature is enabled in Windows Features
- Try a cold boot: `emulator -avd MyEmulator -no-snapshot`

### Android: `adb` not found
- Ensure `$ANDROID_HOME/platform-tools` is on your `PATH`
- Run `adb kill-server && adb start-server` to reset the adb daemon

### Android: Appium cannot find UiAutomator2
- Run `appium driver install uiautomator2` again
- If using a non-default Appium home, set `APPIUM_HOME` to point to the correct directory

### Android: tests time out during session creation
- Ensure the emulator is fully booted before starting tests (`adb devices` shows `device`, not `offline`)
- Increase `connectionRetryTimeout` in your WDIO config
- Verify that the APK path in capabilities is correct and the file exists

### iOS: `xcrun: error: unable to find utility`
- Run `sudo xcode-select --switch /Applications/Xcode.app` to point to the correct Xcode installation

### iOS: simulator not found
- Open Xcode > Settings > Platforms and download the required iOS Simulator runtime
- List available simulators: `xcrun simctl list devices`

### iOS: WebDriverAgent build fails
- Open `~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj` in Xcode
- Set a valid signing team under Signing & Capabilities

### Report not generated
- Ensure `TestRelicService` is listed in `services` **and** `TestRelicReporter` is listed in `reporters` — both are required
- Check that `outputPath` directories are writable by the process

---

## License

MIT — see [LICENSE](LICENSE) for details.

&copy; TestRelic AI &lt;hello@testrelic.ai&gt; — [testrelic.ai](https://testrelic.ai)
