Logs

Overview

  • GPT Driver creates XCTest Attachments which can be directly viewed in XCode after a test run

  • GPT Driver also creates a Session URL which contains a log of each step executed via GPT Driver. This URL is hosted on the GPT Driver web platform.

How to retrieve the GPT Driver Session URL?

The GPTDriver session URL is made available through multiple channels:

  1. OS Logs - Structured logging with .notice level (maps to OSLogType.default)

  2. XCTest Attachments - Saved as .webloc file in test results

  3. Public Property - Accessible programmatically via driver.sessionURL

  4. Callback - onSessionCreated closure for real-time access

The session URL is automatically saved as an XCTAttachment in your test results.

Location in Test Results

The attachment is saved with:

  • Name: GPTDriver Session URL Link

  • Lifetime: .keepAlways (persists even after test completion)

  • UserInfo: Contains ["link": "<session-url>"]

  • File Format: .webloc (macOS web location file in XML format)

Extracting from .xcresult Bundle

Using Python Script

#!/usr/bin/env python3
"""
Extract GPTDriver session URL from .xcresult bundle
"""
import json
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

def extract_session_url_from_xcresult(xcresult_path: str) -> str | None:
    """Extract session URL from .xcresult bundle using xcresulttool."""
    try:
        # Get JSON representation of attachments
        result = subprocess.run(
            ["xcrun", "xcresulttool", "get", "--path", xcresult_path, "--format", "json"],
            capture_output=True,
            text=True,
            check=True
        )
        
        data = json.loads(result.stdout)
        
        # Recursively search for the attachment
        def find_attachment(obj, target_name="GPTDriver Session URL Link"):
            if isinstance(obj, dict):
                if obj.get("name") == target_name:
                    # Check userInfo for link
                    if "userInfo" in obj and "link" in obj["userInfo"]:
                        return obj["userInfo"]["link"]
                    # Or check if there's a payload reference
                    if "payloadRef" in obj:
                        return extract_from_payload(obj["payloadRef"]["id"], xcresult_path)
                # Recursively search
                for value in obj.values():
                    result = find_attachment(value, target_name)
                    if result:
                        return result
            elif isinstance(obj, list):
                for item in obj:
                    result = find_attachment(item, target_name)
                    if result:
                        return result
            return None
        
        def extract_from_payload(id_ref: str, xcresult_path: str) -> str | None:
            """Extract URL from webloc file attachment."""
            try:
                # Export the attachment
                temp_dir = Path("/tmp/xcresult_extract")
                temp_dir.mkdir(exist_ok=True)
                
                subprocess.run(
                    ["xcrun", "xcresulttool", "export", "--path", xcresult_path,
                     "--id", id_ref, "--type", "file", "--output-path", str(temp_dir / "attachment.webloc")],
                    check=True,
                    capture_output=True
                )
                
                # Parse the webloc XML file
                webloc_path = temp_dir / "attachment.webloc"
                if webloc_path.exists():
                    tree = ET.parse(webloc_path)
                    root = tree.getroot()
                    # webloc format: <dict><key>URL</key><string>https://...</string></dict>
                    url_elem = root.find(".//string")
                    if url_elem is not None:
                        return url_elem.text
            except Exception as e:
                print(f"Error extracting payload: {e}", file=sys.stderr)
            return None
        
        return find_attachment(data)
        
    except Exception as e:
        print(f"Error processing xcresult: {e}", file=sys.stderr)
        return None

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: extract_session_url.py <path-to-xcresult>")
        sys.exit(1)
    
    url = extract_session_url_from_xcresult(sys.argv[1])
    if url:
        print(url)
    else:
        print("Session URL not found", file=sys.stderr)
        sys.exit(1)

Using Swift Script

#!/usr/bin/env swift
import Foundation

// Extract session URL from xcresult bundle
func extractSessionURL(from xcresultPath: String) -> String? {
    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
    process.arguments = ["xcresulttool", "get", "--path", xcresultPath, "--format", "json"]
    
    let pipe = Pipe()
    process.standardOutput = pipe
    
    do {
        try process.run()
        process.waitUntilExit()
        
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return nil
        }
        
        // Recursively search for attachment with name "GPTDriver Session URL Link"
        func findAttachment(in obj: Any) -> String? {
            if let dict = obj as? [String: Any] {
                if dict["name"] as? String == "GPTDriver Session URL Link",
                   let userInfo = dict["userInfo"] as? [String: Any],
                   let link = userInfo["link"] as? String {
                    return link
                }
                for value in dict.values {
                    if let result = findAttachment(in: value) {
                        return result
                    }
                }
            } else if let array = obj as? [Any] {
                for item in array {
                    if let result = findAttachment(in: item) {
                        return result
                    }
                }
            }
            return nil
        }
        
        return findAttachment(in: json)
    } catch {
        print("Error: \(error)", to: &FileHandle.standardError)
        return nil
    }
}

// Usage
if CommandLine.arguments.count < 2 {
    print("Usage: extract_session_url.swift <path-to-xcresult>", to: &FileHandle.standardError)
    exit(1)
}

if let url = extractSessionURL(from: CommandLine.arguments[1]) {
    print(url)
} else {
    print("Session URL not found", to: &FileHandle.standardError)
    exit(1)
}

Method 2: Extract from OS Logs

The session URL is logged with .notice level using the unified logging system.

Log Format

The log entry follows this format:

[gptdriver][session:<session-id>] Live Session View | sessionURL=https://app.mobileboost.io/gpt-driver/sessions/<session-id>

Extracting from Log Files

Using log command (macOS)

# Show recent GPTDriver logs
log show --predicate 'subsystem == "com.gptdriver"' --last 1h

# Extract session URLs from logs
log show --predicate 'subsystem == "com.gptdriver" AND eventMessage CONTAINS "Live Session View"' \
  --last 1h | grep -o 'sessionURL=[^|]*' | cut -d= -f2

# Save to file
log show --predicate 'subsystem == "com.gptdriver"' --last 1h > gptdriver_logs.txt
grep "sessionURL" gptdriver_logs.txt | sed 's/.*sessionURL=\([^|]*\).*/\1/'

Using os_log Export (CI)

If you're exporting logs in CI, you can parse them:

# In your CI script, export logs before test completion
xcrun simctl spawn booted log collect --output gptdriver_logs.logarchive 2>/dev/null || true

# Extract session URLs
log show --archive gptdriver_logs.logarchive \
  --predicate 'subsystem == "com.gptdriver" AND eventMessage CONTAINS "Live Session View"' \
  | grep -o 'sessionURL=[^|]*' | cut -d= -f2

Method 3: Programmatic Access

Using the Public Property

let driver = GptDriver(apiKey: "your-api-key")
try await driver.execute("Your command")

// Access session URL directly
if let sessionURL = driver.sessionURL {
    print("Session URL: \(sessionURL)")
    // Upload to CI artifact storage, send to Slack, etc.
}

Using the Callback

let driver = GptDriver(apiKey: "your-api-key")

// Set up callback to capture session URL immediately
driver.onSessionCreated = { sessionURL in
    print("Session created: \(sessionURL)")
    
    // Example: Save to file for CI
    let url = URL(fileURLWithPath: "session_url.txt")
    try? sessionURL.write(to: url, atomically: true, encoding: .utf8)
    
    // Example: Set as environment variable (if running in CI)
    setenv("GPTDRIVER_SESSION_URL", sessionURL, 1)
    
    // Example: Send to CI notification service
    // sendToSlack(sessionURL)
    // postToGitHubActions(sessionURL)
}

try await driver.execute("Your command")

CI Integration Examples

GitHub Actions

name: XCTest CI

on: [push]

jobs:
  test:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Tests
        run: |
          xcodebuild test \
            -scheme YourScheme \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -resultBundlePath TestResults.xcresult
        
      - name: Extract Session URL
        id: extract_url
        run: |
          SESSION_URL=$(xcrun xcresulttool get --path TestResults.xcresult --format json | \
            python3 -c "import sys, json; \
            data = json.load(sys.stdin); \
            def find_url(obj): \
              if isinstance(obj, dict): \
                if obj.get('name') == 'GPTDriver Session URL Link' and 'userInfo' in obj: \
                  return obj['userInfo'].get('link'); \
                for v in obj.values(): \
                  r = find_url(v); \
                  if r: return r; \
              elif isinstance(obj, list): \
                for item in obj: \
                  r = find_url(item); \
                  if r: return r; \
              return None; \
            print(find_url(data) or '')")
          echo "url=$SESSION_URL" >> $GITHUB_OUTPUT
      
      - name: Comment Session URL
        if: steps.extract_url.outputs.url != ''
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `GPTDriver Session: ${{ steps.extract_url.outputs.url }}`
            })
      
      - name: Upload Test Results
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: TestResults.xcresult

Troubleshooting

Session URL Not Found

  1. Check if session was created: Ensure startSession() completed successfully

  2. Verify attachment lifetime: The attachment uses .keepAlways - check your test result bundle settings

  3. Check log level: Ensure .notice level logs are captured in your CI environment

Parsing Issues

  1. JSON format: The xcresulttool JSON output structure may vary - use recursive search

  2. Webloc format: The .webloc file is XML format - use proper XML parsing

  3. Encoding: Ensure UTF-8 encoding when reading files

Performance

  • For large test suites, extracting attachments can be slow

  • Consider using the callback method (onSessionCreated) for real-time access

  • Cache session URLs if running multiple tests

Best Practices

  1. Use the callback: Set onSessionCreated for immediate access

  2. Save to file: Write session URL to a file early in your test run

  3. Include in test reports: Attach session URL to your test reporting tool

  4. Set environment variable: Make session URL available to other CI steps

  5. Archive results: Keep .xcresult bundles for debugging

Last updated