Nikola Brežnjak blog - Tackling software development with a dose of humor
  • Home
  • Daily Thoughts
  • Ionic
  • Stack Overflow
  • Books
  • About me
Home
Daily Thoughts
Ionic
Stack Overflow
Books
About me
  • Home
  • Daily Thoughts
  • Ionic
  • Stack Overflow
  • Books
  • About me
Nikola Brežnjak blog - Tackling software development with a dose of humor
JavaScript

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 💪

Daily Thoughts

Do it when you don’t feel like it

I get it.

Sometimes it’s just hard. Like, legit hard. To do.

To do what?

Well, really, anything that’s not easy.

You’re just stuck. You need a break. A break from being stuck.

But guess what?

That’s exactly the time when you need to do it. To do that thing that you’ve been dreading, or stalling or whatever.

These moments will define you and shape you into a person that does something even when you "don’t feel like it".

It will pay off.

Do it 💪


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

Self-mastery

No matter the level of achievement in external things, mastering oneself is the greatest one.


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

Writing well

Learning to write well is a skill that will come useful no matter the industry.


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts, JavaScript

const life = change();

They (supposedly, originally, Heraclitus) say that the only true constant in life is change.

A JavaScript programmer in you might write that like this:

const life = change();

And, the trick in real life is that the function’s implementation regularly looks like this:

function change() {
    return Math.random();
}

However, when you’d try to output the value of the life constant (see it in action in JS Fiddle), it would be exactly that, a constant (no matter how many times you’d call it or output it).

Sidenote: if you’re puzzled by the fact that you can assign a function to a constant (or to a variable for that matter) before it was defined in the code, then go and learn about hoisting in JavaScript.

Now, you may write the above statement like this:

const life = function change() {
    return Math.random();
}

Now, if you call the life function (again, see it in action), it will return a different value every time you call it.

⚠️ JavaScript gurus among you may chuckle at assigning a function to a constant, but check this StackOverflow answer for its applicability – plus, you’ll learn (or, refresh your memory) about hoisting in JS.

Switching gears; the point of all this is that you can’t expect to be doing the same thing, and getting different results, and no matter what obstacles you face, the key to overcoming them is not in changing the event, but in changing yourself and how you react to it. And that itself is a process. A process that begins with the desire or acceptance to be teachable and improve for the better.

Hope you like this attempt at mixing programming with personal growth topics (for more, check my daily thoughts entries).

Stay safe, friends ❤️


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

Helping others

In the case of an air pressure type emergency on the ✈️, before helping others you should put the mask on first. The same goes for helping others in any other way; help yourself first. How can you help someone with something if you yourself don’t yet know the best way of doing that something?


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

The curious case of tomorrow

This may come as a shock to learn that (as reported here – and there are many similar reports, just Google them):

Fewer than 1 in 100 stroke survivors met all seven heart-health goals identified by the American Heart Association. And just 1 in 5 met four of those goals.

James Clear wrote the other day:

If you’d like to do something bold with your life, you will have to choose to do something bold on a specific day. There is no perfect day. There is no right time. For the trajectory to change, there has to be one day when you simply make the choice.

So, I really wonder, how sick and tired of something we need to be in order to change that something for the better?

Instead of waiting for tomorrow, why not start today?


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

Filter Bubbles

Filter bubbles have a task to keep you engaged consuming instead of increasing your education or quality of life.


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

Going all in

Don’t expect much from x, if you’re just half-assing your commitment to it. Whatever the x may be. Go all in!


This is why I write these ‘daily thoughts’ posts.

Daily Thoughts

Unsocial media

Social media is all but making us social these days.


This is why I write these ‘daily thoughts’ posts.

Page 3 of 51« First...«2345»102030...Last »

Recent posts

  • Discipline is also a talent
  • Play for the fun of it
  • The importance of failing
  • A fresh start
  • Perseverance

Categories

  • Android (3)
  • Books (114)
    • Programming (22)
  • CodeProject (35)
  • Daily Thoughts (77)
  • Go (3)
  • iOS (5)
  • JavaScript (127)
    • Angular (4)
    • Angular 2 (3)
    • Ionic (61)
    • Ionic2 (2)
    • Ionic3 (8)
    • MEAN (3)
    • NodeJS (27)
    • Phaser (1)
    • React (1)
    • Three.js (1)
    • Vue.js (2)
  • Leadership (1)
  • Meetups (8)
  • Miscellaneou$ (77)
    • Breaking News (8)
    • CodeSchool (2)
    • Hacker Games (3)
    • Pluralsight (7)
    • Projects (2)
    • Sublime Text (2)
  • PHP (6)
  • Quick tips (40)
  • Servers (8)
    • Heroku (1)
    • Linux (3)
  • Stack Overflow (81)
  • Unity3D (9)
  • Windows (8)
    • C# (2)
    • WPF (3)
  • Wordpress (2)

"There's no short-term solution for a long-term result." ~ Greg Plitt

"Everything around you that you call life was made up by people that were no smarter than you." ~ S. Jobs

"Hard work beats talent when talent doesn't work hard." ~ Tim Notke

© since 2016 - Nikola Brežnjak