{"id":4604,"date":"2022-03-12T13:25:03","date_gmt":"2022-03-12T13:25:03","guid":{"rendered":"https:\/\/www.nikola-breznjak.com\/blog\/?p=4604"},"modified":"2022-03-16T10:20:46","modified_gmt":"2022-03-16T10:20:46","slug":"using-puppeteer-to-automate-screenshots-in-asc-analytics","status":"publish","type":"post","link":"https:\/\/nikola-breznjak.com\/blog\/javascript\/using-puppeteer-to-automate-screenshots-in-asc-analytics\/","title":{"rendered":"Using Puppeteer to automate ASC Analytics screenshots"},"content":{"rendered":"<h2>TL;DR<\/h2>\n<p>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.<\/p>\n<h2>Why Puppeteer?<\/h2>\n<p>I&#8217;m not affiliated with it in any way, and I also tried <a href=\"https:\/\/www.cypress.io\/\">Cypress<\/a> and <a href=\"https:\/\/www.casperjs.org\/\">CasperJS<\/a> but ran into rather weird problems during installation. Puppeteer just worked straight out of the box, and that&#8217;s why I continued down that path.<\/p>\n<h2>Install Puppeteer<\/h2>\n<p>On <a href=\"https:\/\/github.com\/puppeteer\/puppeteer\">Puppeteer&#8217;s Github page<\/a> it&#8217;s defined as:<\/p>\n<blockquote>\n<p>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.<\/p>\n<\/blockquote>\n<p>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): <\/p>\n<ul>\n<li>Crawl a SPA (Single-Page Application) and generate pre-rendered content, and take screenshots<\/li>\n<li>Automate form submission, UI testing, keyboard input<\/li>\n<li>Create an automated testing environment: run your tests directly in the latest version of Chrome<\/li>\n<\/ul>\n<p>To install it, make sure you have <a href=\"https:\/\/nodejs.org\/en\/\">Node.js<\/a> installed, and then run <code>npm install puppeteer<\/code> in your terminal.<\/p>\n<h2>A simple example using Puppeteer<\/h2>\n<p>Here&#8217;s a simple example to get you started; create a <code>google.js<\/code> file and paste this code in it:<\/p>\n<pre><code>const puppeteer = require(&#039;puppeteer&#039;);\n\n(async () =&gt; {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.goto(&#039;https:\/\/google.com&#039;);\n  await page.screenshot({ path: &#039;google-screenshot.png&#039; });\n\n  await browser.close();\n})();<\/code><\/pre>\n<p>This script will navigate to <a href=\"https:\/\/google.com\">https:\/\/google.com<\/a> and save a screenshot in an image named <code>google-screenshot.png<\/code>.<\/p>\n<p>Execute the script in the command line with: <code>node google.js<\/code> and you should get a PNG image of how the <a href=\"https:\/\/google.com\">https:\/\/google.com<\/a> page would look like in your browser:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/i.imgur.com\/jCUs4rU.png\" alt=\"\" \/><\/p>\n<p>Mind you, the text you&#8217;re seeing is <a href=\"https:\/\/en.wikipedia.org\/wiki\/Croatia\">Croatian<\/a>, as that&#8217;s my locale setting.<\/p>\n<p>Looks a bit &#8216;small&#8217;, doesn&#8217;t it? That&#8217;s because Puppeteer sets an initial page size to <code>800 \u00d7 600 px<\/code>, which defines the screenshot size. The page size can be customized with <a href=\"https:\/\/github.com\/puppeteer\/puppeteer\/blob\/v13.5.1\/docs\/api.md#pagesetviewportviewport\">Page.setViewport()<\/a>.<\/p>\n<p>Here&#8217;s the code example that&#8217;s using <code>1920 x 1080 px<\/code> viewport size:<\/p>\n<pre><code>const puppeteer = require(&#039;puppeteer&#039;);\n\n(async () =&gt; {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  await page.setViewport({\n    width: 1920,\n    height: 1080,\n    deviceScaleFactor: 1,\n  });\n\n  await page.goto(&#039;https:\/\/google.com&#039;);\n  await page.screenshot({ path: &#039;google-screenshot.png&#039; });\n\n  await browser.close();\n})();<\/code><\/pre>\n<p>Run it again with <code>node google.js<\/code>, and the screenshot should now look like this: <\/p>\n<p><img decoding=\"async\" src=\"https:\/\/i.imgur.com\/dZqIrhR.png\" alt=\"\" \/><\/p>\n<p>For more options (as I won&#8217;t go into explaining every API call of the final code) make sure to check <a href=\"https:\/\/pptr.dev\/\">their documentation<\/a>.<\/p>\n<h2>The Code\u2122<\/h2>\n<p>Below is the quick and dirty code that JustWorks\u2122 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.<\/p>\n<p>I say quick and dirty because there are a myriad of improvements that could be done on this code:<\/p>\n<ul>\n<li>Cookie support (so you don&#8217;t have to log in and insert the OTP every time)<\/li>\n<li>Typescript (for type safety, and <a href=\"https:\/\/www.google.com\/search?q=why+is+typescript+better+than+javascript\">other benefits<\/a>)<\/li>\n<\/ul>\n<p>The code is also available <a href=\"https:\/\/github.com\/Hitman666\/ASC-Analytics-Screenshots-With-Puppeteer\">on Github<\/a>, and if you&#8217;ve got the skills and the desire, please make those changes and <a href=\"https:\/\/github.com\/Hitman666\/ASC-Analytics-Screenshots-With-Puppeteer\/compare\">create a PR<\/a>.<\/p>\n<pre><code>const ACCOUNT = &#039;your.email@address.com&#039;;\nconst PASSWORD = &#039;YourVeryMuchSuperStrongPa$$word&#039;;\nconst LOGIN_FRAME = &#039;#aid-auth-widget-iFrame&#039;;\nconst ASC_URL = &#039;https:\/\/appstoreconnect.apple.com\/&#039;;\nconst ANALYTICS_URL = &#039;https:\/\/appstoreconnect.apple.com\/analytics&#039;;\nconst APP_CRASHES_URL = &#039;theSpecificUrlToYourSpecificApp&#039;; \/\/figure this out by copying the URL manually\n\nconst puppeteer = require(&#039;puppeteer&#039;);\nconst readline = require(&#039;readline&#039;);\n\nconst log = (msg) =&gt; {\n    console.log(msg);\n}\n\nconst clickSignInButton = async (frame) =&gt; {\n    log(&#039;Clicked the Sign In button&#039;);\n\n    const element = await frame.waitForSelector(\n        &#039;#stepEl &gt; sign-in &gt; #signin &gt; .container &gt; #sign-in:not(disabled)&#039;\n    );\n\n    await element.click();\n};\n\nconst clickTrustBrowser = async (frame) =&gt; {\n    log(&#039;Clicked the Trust Browser button&#039;);\n\n    const selector = &#039;button.trust-browser&#039;;\n    const element = await frame.waitForSelector(selector);\n    await element.click();\n};\n\nconst askForVerificationCode = () =&gt; {\n    log(&#039;Asking for verification code&#039;);\n\n    const readlineInterface = readline.createInterface({\n        input: process.stdin,\n        output: process.stdout,\n    });\n\n    return new Promise(resolve =&gt; {\n        readlineInterface.question(\n            &#039;Please enter your code: &#039;,\n            answer =&gt; {\n                console.log(`Thanks, you entered: ${answer}`);\n                readlineInterface.close();\n                resolve(answer);\n            }\n    );\n  });\n};\n\nconst login = async (page, user, password) =&gt; {\n    log(&#039;Login page&#039;);\n    const frameElement = await page.$(LOGIN_FRAME);\n    if (!frameElement) {\n        throw new Error(`Missing frame ${LOGIN_FRAME}`);\n    }\n\n    const frame = await frameElement.contentFrame();\n    if (!frame) {\n        throw new Error(`Missing frame ${LOGIN_FRAME}`);\n    }\n\n    const ACCOUNTInputSelector = &#039;#account_name_text_field&#039;;\n    await frame.waitForSelector(ACCOUNTInputSelector);\n\n    await frame.focus(ACCOUNTInputSelector);\n    await page.keyboard.type(user);\n\n    await clickSignInButton(frame);\n\n    const passwordInputSelector = &#039;#password_text_field&#039;;\n    await frame.waitForSelector(passwordInputSelector);\n    await frame.waitForTimeout(2000);\n\n    await frame.focus(passwordInputSelector);\n    await page.keyboard.type(password);\n    await clickSignInButton(frame);\n\n    const verifyDeviceSelector = &#039;verify-phone&#039;;\n    await frame.waitForSelector(`${verifyDeviceSelector}`);\n    const isVerifyDevice = await frame.$(verifyDeviceSelector);\n\n    if (isVerifyDevice) {\n        log(&#039;Verify phone step&#039;);\n        const verificationCode = await askForVerificationCode();\n        await page.keyboard.type(verificationCode);\n        await clickTrustBrowser(frame);\n    }\n};\n\nconst main = async () =&gt; {\n    const browser = await puppeteer.launch({\n        \/\/ set this to false if you want to open a browser instance and see what your\n        \/\/ script is doing, where it&#039;s clicking, what it&#039;s filling out, etc.\n        headless: false \n    });\n\n    const page = await browser.newPage();\n    await page.setViewport({\n        \/\/ settings for my personal need, set this as you wish\n        width: 1440,\n        height: 815,\n        deviceScaleFactor: 1,\n    });\n\n    log(`Oppening ${ASC_URL}`);\n    await page.goto(ASC_URL);\n\n    await page.waitForSelector(`${LOGIN_FRAME}`);\n    await login(page, ACCOUNT, PASSWORD);\n\n    await page.waitForNavigation({ waitUntil: &#039;networkidle2&#039; });\n    await page.waitForSelector(&#039;.main-nav-label&#039;);\n\n    await page.goto(`${ANALYTICS_URL}`);\n    await page.goto(`${APP_CRASHES_URL}`);\n\n    await page.waitForNavigation({ waitUntil: &#039;networkidle2&#039; });\n    await page.waitForSelector(&#039;#appanalytics&#039;);\n    \/\/ sometimes the selector will load, but not the whole content, so you may\n    \/\/ want to play with the waitForTimeout. The argument is in mili seconds\n    \/\/await page.waitForTimeout(5000); \n\n    await page.screenshot({ path: &#039;app_analytics_crashes.png&#039; });\n\n    log(&#039;Closing the browser page&#039;);\n    await browser.close();\n};\n\nmain().catch(error =&gt; console.error(error));<\/code><\/pre>\n<p>When you run this code, in the terminal you should see this output:<\/p>\n<pre><code>\u279c node apple.js               \nOppening https:\/\/appstoreconnect.apple.com\/\nLogin page\nClicked the Sign In button\nClicked the Sign In button\nVerify phone step\nAsking for verification code\nPlease enter your code: 866216\nThanks, you entered: 866216\nClicked the Trust Browser button\nClosing the browser page<\/code><\/pre>\n<p>The OTP code to log in to Apple, in my case, was sent to me via an SMS.<\/p>\n<p>The screenshot (blurred for obvious reasons), looks like this in my case:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/i.imgur.com\/8aO58hI.png\" alt=\"\" \/><\/p>\n<h2>That&#8217;s great, but what if my login requires CAPTCHA?<\/h2>\n<p>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).<\/p>\n<p>But, what if your login requires you to enter the CAPTCHA? You know, those &quot;are you human&quot; popups that you get when logging into some website. Then I had a &#8216;great idea&#8217; &#8211; what if you make a service where people actually read and type these CAPTCHAs for you?<\/p>\n<blockquote>\n<p>\u26a0\ufe0f Now, let me be very clear &#8211; just because something is possible to do automatically, it doesn&#8217;t give you the right to use that in a bad way (spammers be gone!). Do with this information what you wish, but don&#8217;t come crying back saying &quot;he made me do it&quot;.<\/p>\n<\/blockquote>\n<p>Yes, no drumrolls needed, as almost everything today &#8211; this also already exists. CAPTCHA solving software exists and it&#8217;s a thing, and I found a blog posts that reviews <a href=\"https:\/\/prowebscraper.com\/blog\/top-10-captcha-solving-services-compared\/\">10 CAPTCHA-solving software<\/a>.<\/p>\n<p>One that caught my eye was <a href=\"https:\/\/2captcha.com\/\">2captcha.com<\/a>, and I found a <a href=\"https:\/\/www.bloggersideas.com\/2Captcha-Review\/\">blog post<\/a> 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).<\/p>\n<h3>A trip down the memory lane<\/h3>\n<blockquote>\n<p>Sorry for the sidetrack (and you can safely skip this section \ud83d\ude05), but I just remembered how, years ago, I attended a conference where Photomath&#8217;s (the app that solves math problems and shows you the steps of how to solve them) CEO was talking about how they&#8217;re solving the OCR problem (they supposedly had a breakthrough when they applied ML). They built an awesome business, that&#8217;s actually useful, and I saw that they recently integrated into Snapchat via their Snapchat Solver \ud83e\udd2f . I digress, yes, but with that good &#8216;tech&#8217;, I see them integrated into everything and that makes me proud (since, at the time, they were a Croatian-based startup).<\/p>\n<\/blockquote>\n<h2>Conclusion<\/h2>\n<p>Hope this straight-to-the-point post (with one slight detour \ud83d\ude05) showed you how you can make use of the amazing Puppeteer for your (legit) purposes.<\/p>\n<p>Stay safe and code on \ud83d\udcaa<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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&hellip;<\/p>\n","protected":false},"author":1,"featured_media":4605,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[27],"tags":[],"class_list":["post-4604","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-javascript"],"_links":{"self":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/4604","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/comments?post=4604"}],"version-history":[{"count":5,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/4604\/revisions"}],"predecessor-version":[{"id":4612,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/4604\/revisions\/4612"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/media\/4605"}],"wp:attachment":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/media?parent=4604"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/categories?post=4604"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/tags?post=4604"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}