Using Puppeteer to automate ASC Analytics screenshots

TL;DR

I was trying to automate my manual process of logging into ASC (App Store Connect) and taking a screenshot of the crashes, and preparing a report that I do weekly. This blog post explains the exact steps of how I achieved that.

Why Puppeteer?

I’m not affiliated with it in any way, and I also tried Cypress and CasperJS but ran into rather weird problems during installation. Puppeteer just worked straight out of the box, and that’s why I continued down that path.

Install Puppeteer

On Puppeteer’s Github page it’s defined as:

a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

Here are a few examples of what you can do with Puppeteer (although they say that you can automate almost everything that you do manually in the browser):

  • Crawl a SPA (Single-Page Application) and generate pre-rendered content, and take screenshots
  • Automate form submission, UI testing, keyboard input
  • Create an automated testing environment: run your tests directly in the latest version of Chrome

To install it, make sure you have Node.js installed, and then run npm install puppeteer in your terminal.

A simple example using Puppeteer

Here’s a simple example to get you started; create a google.js file and paste this code in it:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://google.com');
  await page.screenshot({ path: 'google-screenshot.png' });

  await browser.close();
})();

This script will navigate to https://google.com and save a screenshot in an image named google-screenshot.png.

Execute the script in the command line with: node google.js and you should get a PNG image of how the https://google.com page would look like in your browser:

Mind you, the text you’re seeing is Croatian, as that’s my locale setting.

Looks a bit ‘small’, doesn’t it? That’s because Puppeteer sets an initial page size to 800 × 600 px, which defines the screenshot size. The page size can be customized with Page.setViewport().

Here’s the code example that’s using 1920 x 1080 px viewport size:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setViewport({
    width: 1920,
    height: 1080,
    deviceScaleFactor: 1,
  });

  await page.goto('https://google.com');
  await page.screenshot({ path: 'google-screenshot.png' });

  await browser.close();
})();

Run it again with node google.js, and the screenshot should now look like this:

For more options (as I won’t go into explaining every API call of the final code) make sure to check their documentation.

The Code™

Below is the quick and dirty code that JustWorks™ in logging into your App Store Connect account (please make sure to edit the script with your credentials), navigating to the Analytics screen and taking a screenshot.

I say quick and dirty because there are a myriad of improvements that could be done on this code:

  • Cookie support (so you don’t have to log in and insert the OTP every time)
  • Typescript (for type safety, and other benefits)

The code is also available on Github, and if you’ve got the skills and the desire, please make those changes and create a PR.

const ACCOUNT = '[email protected]';
const PASSWORD = 'YourVeryMuchSuperStrongPa$$word';
const LOGIN_FRAME = '#aid-auth-widget-iFrame';
const ASC_URL = 'https://appstoreconnect.apple.com/';
const ANALYTICS_URL = 'https://appstoreconnect.apple.com/analytics';
const APP_CRASHES_URL = 'theSpecificUrlToYourSpecificApp'; //figure this out by copying the URL manually

const puppeteer = require('puppeteer');
const readline = require('readline');

const log = (msg) => {
    console.log(msg);
}

const clickSignInButton = async (frame) => {
    log('Clicked the Sign In button');

    const element = await frame.waitForSelector(
        '#stepEl > sign-in > #signin > .container > #sign-in:not(disabled)'
    );

    await element.click();
};

const clickTrustBrowser = async (frame) => {
    log('Clicked the Trust Browser button');

    const selector = 'button.trust-browser';
    const element = await frame.waitForSelector(selector);
    await element.click();
};

const askForVerificationCode = () => {
    log('Asking for verification code');

    const readlineInterface = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });

    return new Promise(resolve => {
        readlineInterface.question(
            'Please enter your code: ',
            answer => {
                console.log(`Thanks, you entered: ${answer}`);
                readlineInterface.close();
                resolve(answer);
            }
    );
  });
};

const login = async (page, user, password) => {
    log('Login page');
    const frameElement = await page.$(LOGIN_FRAME);
    if (!frameElement) {
        throw new Error(`Missing frame ${LOGIN_FRAME}`);
    }

    const frame = await frameElement.contentFrame();
    if (!frame) {
        throw new Error(`Missing frame ${LOGIN_FRAME}`);
    }

    const ACCOUNTInputSelector = '#account_name_text_field';
    await frame.waitForSelector(ACCOUNTInputSelector);

    await frame.focus(ACCOUNTInputSelector);
    await page.keyboard.type(user);

    await clickSignInButton(frame);

    const passwordInputSelector = '#password_text_field';
    await frame.waitForSelector(passwordInputSelector);
    await frame.waitForTimeout(2000);

    await frame.focus(passwordInputSelector);
    await page.keyboard.type(password);
    await clickSignInButton(frame);

    const verifyDeviceSelector = 'verify-phone';
    await frame.waitForSelector(`${verifyDeviceSelector}`);
    const isVerifyDevice = await frame.$(verifyDeviceSelector);

    if (isVerifyDevice) {
        log('Verify phone step');
        const verificationCode = await askForVerificationCode();
        await page.keyboard.type(verificationCode);
        await clickTrustBrowser(frame);
    }
};

const main = async () => {
    const browser = await puppeteer.launch({
        // set this to false if you want to open a browser instance and see what your
        // script is doing, where it's clicking, what it's filling out, etc.
        headless: false 
    });

    const page = await browser.newPage();
    await page.setViewport({
        // settings for my personal need, set this as you wish
        width: 1440,
        height: 815,
        deviceScaleFactor: 1,
    });

    log(`Oppening ${ASC_URL}`);
    await page.goto(ASC_URL);

    await page.waitForSelector(`${LOGIN_FRAME}`);
    await login(page, ACCOUNT, PASSWORD);

    await page.waitForNavigation({ waitUntil: 'networkidle2' });
    await page.waitForSelector('.main-nav-label');

    await page.goto(`${ANALYTICS_URL}`);
    await page.goto(`${APP_CRASHES_URL}`);

    await page.waitForNavigation({ waitUntil: 'networkidle2' });
    await page.waitForSelector('#appanalytics');
    // sometimes the selector will load, but not the whole content, so you may
    // want to play with the waitForTimeout. The argument is in mili seconds
    //await page.waitForTimeout(5000); 

    await page.screenshot({ path: 'app_analytics_crashes.png' });

    log('Closing the browser page');
    await browser.close();
};

main().catch(error => console.error(error));

When you run this code, in the terminal you should see this output:

➜ node apple.js               
Oppening https://appstoreconnect.apple.com/
Login page
Clicked the Sign In button
Clicked the Sign In button
Verify phone step
Asking for verification code
Please enter your code: 866216
Thanks, you entered: 866216
Clicked the Trust Browser button
Closing the browser page

The OTP code to log in to Apple, in my case, was sent to me via an SMS.

The screenshot (blurred for obvious reasons), looks like this in my case:

That’s great, but what if my login requires CAPTCHA?

In my particular case, the login was rather simple (except for the fact that I had to select the frame and search within it for the element IDs).

But, what if your login requires you to enter the CAPTCHA? You know, those "are you human" popups that you get when logging into some website. Then I had a ‘great idea’ – what if you make a service where people actually read and type these CAPTCHAs for you?

⚠️ Now, let me be very clear – just because something is possible to do automatically, it doesn’t give you the right to use that in a bad way (spammers be gone!). Do with this information what you wish, but don’t come crying back saying "he made me do it".

Yes, no drumrolls needed, as almost everything today – this also already exists. CAPTCHA solving software exists and it’s a thing, and I found a blog posts that reviews 10 CAPTCHA-solving software.

One that caught my eye was 2captcha.com, and I found a blog post that did a thorough teardown of the service. The reason that one caught my eye was that it seems that they offer their solution through libraries for most of the popular programming languages (Python, PHP, JS,Go, C#, Ruby).

A trip down the memory lane

Sorry for the sidetrack (and you can safely skip this section 😅), but I just remembered how, years ago, I attended a conference where Photomath’s (the app that solves math problems and shows you the steps of how to solve them) CEO was talking about how they’re solving the OCR problem (they supposedly had a breakthrough when they applied ML). They built an awesome business, that’s actually useful, and I saw that they recently integrated into Snapchat via their Snapchat Solver 🤯 . I digress, yes, but with that good ‘tech’, I see them integrated into everything and that makes me proud (since, at the time, they were a Croatian-based startup).

Conclusion

Hope this straight-to-the-point post (with one slight detour 😅) showed you how you can make use of the amazing Puppeteer for your (legit) purposes.

Stay safe and code on 💪

Written by Nikola Brežnjak