<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: browserless.io</title>
    <description>The latest articles on DEV Community by browserless.io (@browserless).</description>
    <link>https://dev.to/browserless</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F5432%2Fa56ae08f-ed41-4538-bc56-d30a2fa7c334.png</url>
      <title>DEV Community: browserless.io</title>
      <link>https://dev.to/browserless</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/browserless"/>
    <language>en</language>
    <item>
      <title>Announcing the 2024 Browser Conference</title>
      <dc:creator>Zach Goldie</dc:creator>
      <pubDate>Fri, 17 May 2024 09:38:52 +0000</pubDate>
      <link>https://dev.to/browserless/announcing-the-2024-browser-conference-3o17</link>
      <guid>https://dev.to/browserless/announcing-the-2024-browser-conference-3o17</guid>
      <description>&lt;p&gt;We're so excited to be hosting a second edition of The Browser Conference.&lt;/p&gt;

&lt;p&gt;If you use browser automation for testing, scraping or AI, come join us for a day of free, online talks. Speakers and topics include:&lt;/p&gt;

&lt;p&gt;Google - Next gen protocols with WebDriver BiDi&lt;br&gt;
Sauce Labs - Using AI to speed up test development&lt;br&gt;
Newo.ai - Creating AI agents that can access websites&lt;br&gt;
Oxylabs - Overcoming blocks in large-scale scraping&lt;br&gt;
Selenium - Choosing the right library for your testing&lt;/p&gt;

&lt;p&gt;The conference is on June 20th (with replays available), check out the site to reserve your spot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.browserconference.com/"&gt;browserconference.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our aim for the conference&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our overall goal for the event is to form a central point to talk about using browsers. While there are events based around a particular product or library, we found ourselves wanting one that was common ground for everyone.&lt;/p&gt;

&lt;p&gt;Last year's event was a great first step, so now we've moved things up a level. With developers from key technologies giving talks, we're hoping this can grow to a key event in the world of browser automation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb8s7njjm8o8zyw71kcir.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb8s7njjm8o8zyw71kcir.png" alt="The browser conference, a free online event on June 20th" width="800" height="859"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Is there anyone you'd love to hear a talk from around this subject? If so, please leave a comment below. We'd keen to hear how to make this the best event for everyone.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>selenium</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>How and why we ripped our Open Source product apart for a full rebuild</title>
      <dc:creator>Zach Goldie</dc:creator>
      <pubDate>Wed, 28 Feb 2024 16:27:30 +0000</pubDate>
      <link>https://dev.to/browserless/how-and-why-we-ripped-our-open-source-product-apart-for-a-full-rebuild-552n</link>
      <guid>https://dev.to/browserless/how-and-why-we-ripped-our-open-source-product-apart-for-a-full-rebuild-552n</guid>
      <description>&lt;p&gt;I wanted to share the story about our battle with tech debt and the challenge of completely rebuilding Browserless. It feels in the spirit of open source to share the process as well as the product.&lt;/p&gt;

&lt;p&gt;So, here it goes…&lt;/p&gt;

&lt;h2&gt;
  
  
  Some context about the product and the challenge
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.browserless.io/"&gt;Browserless.io&lt;/a&gt; is an eight person, bootstrapped startup. Things are all going well, we’ve got thousands of users and last year we broke $1M ARR.&lt;/p&gt;

&lt;p&gt;The core product is managed, cloud hosted browsers. We run thousands at a time using AWS and DigitalOcean, for people to use with Puppeteer and Playwright scripts. Our &lt;a href="https://github.com/browserless/browserless"&gt;container&lt;/a&gt; is also available to self deploy under an open-source license.&lt;/p&gt;

&lt;p&gt;Hosting headless browsers is &lt;a href="https://www.browserless.io/blog/2023/12/05/advanced-issues-when-managing-chrome-on-aws/"&gt;notoriously difficult&lt;/a&gt;. They aren’t really designed for cloud deployments in the same way that a databse is.&lt;/p&gt;

&lt;p&gt;Running Browserless has involved challenges such as writing our own load balancers from scratch using NGINX and Lua, while supporting multiple libraries. Any and all rewrites are tricky and need to be approached with caution, which is where our story really starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  V1 was getting pretty creaky
&lt;/h2&gt;

&lt;p&gt;Our founder started building Browserless back in 2015. That means V1 was eight years old and had some key headaches.&lt;/p&gt;

&lt;p&gt;The design patterns we used are now quite legacy and are prone to memory leaks. For example, take a look at the &lt;code&gt;puppeteer-provider&lt;/code&gt; on &lt;a href="https://github.com/browserless/browserless/blob/v1/src/puppeteer-provider.ts"&gt;our V1 branch&lt;/a&gt;. It doesn’t use classes well, calls out to other internal private functions, and is a pain to debug where problems might occur. Its “statefulness” is a somewhat decided anti-pattern, and a clear indicator that a reworking might be necessary.&lt;/p&gt;

&lt;p&gt;There’s also &lt;code&gt;chrome-helper&lt;/code&gt; that “helps” manage chrome. It had similar issues to the &lt;code&gt;puppeteer-provider&lt;/code&gt; in that it’s a stateful piece of code that is very difficult to pinpoint issues on. In general, cleanup happened all over the place when things go wrong, and V1 had to twist and contort the “queue” library in order to implement things like cleanup actions.&lt;/p&gt;

&lt;p&gt;To be quite frank it had just become too unwieldy.&lt;/p&gt;

&lt;p&gt;It was also very strongly tied to Chrome. We wanted to add support for Firefox and Webkit , but V1 heavily relied on the underlying browser to be Chrome for interactions which made this extremely difficult. Assumptions can build all sorts of corners you can code yourself into! Not only that, all of our subdomains, docker tags, and more also relied on this fact.&lt;/p&gt;

&lt;p&gt;The developer experience for our open source image was also in need of improvement.&lt;/p&gt;

&lt;p&gt;V1 had partial support for implementing your own behaviors via hooks, so users could extend the docker image and add their own functionality. It was very clunky to work with and was very limited in terms of what it could do.&lt;/p&gt;

&lt;p&gt;Since we “dogfood” these open source images internally to build our paid product, it was also impacting our small dev team. Building and launching new features was a struggle due to our own technical debt, so a decision was needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accepting it’s time to pay our (tech) debts
&lt;/h2&gt;

&lt;p&gt;Fixing the issues above required a complete overhaul of our core product and underlying platform. It would take months, during which we wouldn’t be shipping many new features. Even launching V2 was mostly going to benefit ourselves at first, with the difference to our paying users only coming once we built new features on top of it.&lt;/p&gt;

&lt;p&gt;Thankfully bootstrapping meant that our MRR was steady and covered all of our costs, so there wasn’t a VC funded runway to race against. But, it would also make it hard to increase revenue during that time*.&lt;/p&gt;

&lt;p&gt;As you can imagine, we discussed this with our advisers multiple times and chewed the decision over for months before finally biting the bullet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;*Side note: at this point we also didn’t have a dedicated marketer and were DIY-ing our promo, but that’s a story for another time.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting together a wishlist
&lt;/h2&gt;

&lt;p&gt;As well as the key factors I’ve already mentioned, we had a pretty lengthy wishlist for V2:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;v1&lt;/th&gt;
&lt;th&gt;v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All images used Chrome or Chromium&lt;/td&gt;
&lt;td&gt;Wanted to have browser-specific images as well as one for all the browsers (Chrome, Firefox and Webkit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Hub’s UI and admin portals have been frustrating, plus users disliked that Docker Hub required a paid account to pull more than a certain amount a month&lt;/td&gt;
&lt;td&gt;Moving to a new docker container repository needed to be possible, instead of being locked in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;We had some legacy sandboxes for securing certain APIs, but they were now deprecated&lt;/td&gt;
&lt;td&gt;$We wanted to shift to more secure methods and stop relying on vulnerable modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;We had to rely on an HTTP framework&lt;/td&gt;
&lt;td&gt;A better dev ex was planned, with auto-generated documented and runtime validation for our APIs that we could implement ourselves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Like many teams, we were dependent on the npm and javascript ecosystem, using hundreds of packages that sometimes went extinct.&lt;/td&gt;
&lt;td&gt;The aim was to use as few modules as possible to ensure stability and security, while not having to rebuild “the hard parts”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More and more packages are moving to ECMAScript modules, which we couldn’t handle well since V1 is CommonJS-based.&lt;/td&gt;
&lt;td&gt;ECMAScript modules are a lot easier “from the ground up.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used various frameworks and routing.&lt;/td&gt;
&lt;td&gt;Custom libraries and routing. NodeJS HTTP service, light and flexible to support both WebSocket and HTTP routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript but no NPM package or SDK for extensions.&lt;/td&gt;
&lt;td&gt;TypeScript with an npm package, for easily building new features&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There was no way to feed all of these things into Version 1 in an elegant way, it could only be done if we stripped the whole thing back to basics.&lt;/p&gt;

&lt;p&gt;We also had a few things we wanted to get rid of as well. As our first major version change, this presented a good opportunity to do so plus write a platform of sorts to tackle things we wanted. You might say it was a great confluence of events to rip this bandaid off.&lt;/p&gt;

&lt;p&gt;The other big decision was to drop support for Selenium. Even though it’s still a popular library, attention is shifting more and more towards Playwright and Puppeteer. Supporting Selenium had far too high a technical cost and complexity, and added yet-another-version to align all things with, so we bit the bullet and dropped it for V2.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft6ojqf431rob7yf07cyr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft6ojqf431rob7yf07cyr.png" alt="Why we're dropping support for Selenium" width="800" height="614"&gt;&lt;/a&gt; &lt;em&gt;Why we're dropping support for Selenium&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan of attack
&lt;/h2&gt;

&lt;p&gt;We put together a plan for the rebuild, the general stages were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Figure out what we didn’t want to build.&lt;/li&gt;
&lt;li&gt;How to handle potentially several browsers per container.&lt;/li&gt;
&lt;li&gt;Make it automatic: documentation generation, OpenAPI schemas, and make it machine readable.&lt;/li&gt;
&lt;li&gt;Make it accessible: deploy to Github Container registry with easy to understand tags and formatting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unsurprisingly, we actually used a lot of the same packages from V1 in support of V2. TypeScript, queue, puppeteer/playwright, and of course prettier and ESLint.&lt;/p&gt;

&lt;p&gt;We desperately wanted to use an API framework, but none really seemed to meet our needs of TypeScript-first, WebSocket routing, and some kind of “dependency” characteristics. As much as it sucked, we needed to write our own. This wasn’t as easy as we’d hoped, and with every great plan there comes an even greater punch to the face.&lt;/p&gt;

&lt;h2&gt;
  
  
  The best laid plans of mice and devs
&lt;/h2&gt;

&lt;p&gt;I believe it was Mike Tyson who said &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everyone has a plan until they get punched in the repo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i1pofq8w2igz4z8asy7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i1pofq8w2igz4z8asy7.png" alt="Everyone has a plan until they get punched in the repo" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yup, things went wrong. Here’s a highlight of some favorite headaches we came across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Having a TypeScript-first approach meant we had to build a bunch of tooling. I won’t go into details as you can freely look at it in our scripts folder here to see for yourselves: &lt;a href="https://github.com/browserless/browserless/tree/main/scripts"&gt;https://github.com/browserless/browserless/tree/main/scripts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Handling WebSocket routes and HTTP routes is tricky and easy to get wrong. What happens when one dies halfway through the request? What if they need a browser to run? What if they don’t? We had to account for a whole bunch of scenarios.&lt;/li&gt;
&lt;li&gt;How do we make the system malleable to routes sometimes being there or not? We wanted custom docker images with only the functionality they needed. Building a lazy-loading route system was difficult to get working 100%.&lt;/li&gt;
&lt;li&gt;Figuring out our priorities for routing was tricky, but worth the several-times-over redesigns. What do developers care about? Headers, Methods, Accepts, Content-types and more can play wildly into how a request is handled.&lt;/li&gt;
&lt;li&gt;Design patterns we’ve seen across other libraries worked great for your cookie-cutter REST APIs, but tend to leave a lot to be desired when working with things like a web-browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It wasn’t all nightmares though. Once we got things set right, not only did it “feel” good, but writing new API routes and functionality happened much much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprising benefits we encountered
&lt;/h2&gt;

&lt;p&gt;Having a class-based routing system, with TypeScript at the center, felt natural and the way forward.&lt;/p&gt;

&lt;p&gt;We now define routes in browserless as being either WebSocket, HTTP or both + a browser. When having a mostly-declarative syntax for routes, and a strong class-based system, other things became a lot easier. &lt;/p&gt;

&lt;p&gt;For example, since we know ahead of time what methods, paths, and request/response formats are we can much more easily generate a live documentation site that details all these items without having to manually create one. This includes query-parameters, response objects or types, and POST body requirements. This value is further shown by the fact that all of these features are also propagated down to the SDK level as well.&lt;/p&gt;

&lt;p&gt;The most surprising thing, however, is that the way we generate our runtime validation passes through meta-data from playwright and puppeteer all the way to the documentation site. That means if there’s a helpful comment, type info, or description in these libraries it’ll surface in documentation. Any updates there are automatically carried over to our platform and passed through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hitting our launch deadline
&lt;/h2&gt;

&lt;p&gt;I’m especially proud (&lt;em&gt;and even a bit surprised&lt;/em&gt;) that we managed to hit our target launch date 12 months after we started. With a long history of use-cases and complexity, recreating the platform was no easy task.&lt;/p&gt;

&lt;p&gt;We were sponsoring a newsletter on Dec 7th last year, so the aim was to ship v2 before then. We managed to ship and make v2 available on GitHub’s container image registry.&lt;/p&gt;

&lt;p&gt;It’s even mostly compatible with v1, so we heard from our self-hosted customers that the transition was fairly simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was a huge undertaking and I’m extremely proud of what we’ve achieved. Overall it took around 400 days of developer time and the payoff is already looking huge.&lt;/p&gt;

&lt;p&gt;The new modular SDK means we’re rapidly developing a range of new features that are in the pipelines, such as enterprise level account management and advanced browser workflows.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>startup</category>
      <category>playwright</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to automate PDF generation of dashboards/web pages with open-source Puppeteer web automation tool</title>
      <dc:creator>Ilya Azovtsev</dc:creator>
      <pubDate>Mon, 09 May 2022 11:47:29 +0000</pubDate>
      <link>https://dev.to/browserless/how-to-automate-pdf-generation-of-dashboardsweb-pages-with-open-source-puppeteer-web-automation-tool-4l1a</link>
      <guid>https://dev.to/browserless/how-to-automate-pdf-generation-of-dashboardsweb-pages-with-open-source-puppeteer-web-automation-tool-4l1a</guid>
      <description>&lt;p&gt;&lt;strong&gt;Send your clients a weekly or monthly report through email – they’ll be grateful!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;“Why do you pay for this product? What’s the real value, in numbers, of using this product?”.&lt;/p&gt;

&lt;p&gt;If you ask your clients this question, and they don’t know the “value” they get in numbers (like hours saved or clients generated with your tool) – they’re in a risk zone for churn 😬&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MCz-WuKX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kx3tzfi55ycgn1ufvo22.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MCz-WuKX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kx3tzfi55ycgn1ufvo22.gif" alt="Image description" width="250" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every SaaS company has dashboards and metrics that they share with clients on their platform. This is the place where clients actually understand how your tool or service helps them.&lt;/p&gt;

&lt;p&gt;Today, we’ll share with you how to save your clients’ time by sending report PDFs automatically to them so they don’t have to fetch them manually.&lt;/p&gt;

&lt;p&gt;And if you ask: “Why do I actually need it?”.&lt;/p&gt;

&lt;p&gt;I’ll tell you, this can improve your retention metrics:&lt;/p&gt;

&lt;p&gt;_Your clients automatically get reports -&amp;gt; they feel value from using your product/service -&amp;gt; they stay with you for a longer time -&amp;gt; you increase the Lifetime Value of the client _🚀&lt;/p&gt;

&lt;p&gt;Clients are always grateful when you can automate repetitive tasks such as downloading reports from your platform. This will only take about an hour to accomplish with browserless and will save your customers precious time.&lt;/p&gt;

&lt;p&gt;We’ll share with you a &lt;a href="https://dev.topuppeteer%20pdf%20generator"&gt;puppeteer pdf generator&lt;/a&gt; *&lt;em&gt;code snippet that you can copy &amp;amp; paste and save time even on coding *&lt;/em&gt;😅&lt;/p&gt;

&lt;h2&gt;
  
  
  How to automate PDF generation with Browserless
&lt;/h2&gt;

&lt;p&gt;Let’s illustrate this with our own browserless’ account dashboard. We can see the number of sessions that have run and our worker’s CPU &amp;amp; memory statistics. We’ll use our browserless API key to run the script and get the PDF returned.&lt;/p&gt;

&lt;p&gt;You can then make a simple NodeJS app to schedule that task and also send the PDF returned from browserless through email. Or you can send these PDFs via your current Email Marketing tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SE9soGGw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipwque39gdw85c07n0ae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SE9soGGw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipwque39gdw85c07n0ae.png" alt="Image description" width="880" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s Get Started&lt;/p&gt;

&lt;p&gt;First of all, create your &lt;a href="https://www.browserless.io/sign-up"&gt;Browserless account&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1skQO6Cd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dm5e3h0x7uqniqrrxx3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1skQO6Cd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dm5e3h0x7uqniqrrxx3z.png" alt="Image description" width="880" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose the plan you want to go with; Usage-based with a free trial or Dedicated.&lt;/p&gt;

&lt;p&gt;Once you create the account, you’ll have the Dashboard (yes, we also have a dashboard 😀) with an API key, that you can use for automation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ffcpcobk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qv8vb98sehltsnyexcw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ffcpcobk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qv8vb98sehltsnyexcw.png" alt="Image description" width="880" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To extract this dashboard, we can use the /function API to run the script below, where “token” is your API key from Browserless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const puppeteer = require('puppeteer-core');

(async() =&amp;gt; {
    const token = "YOUR_API_KEY";
    const email = "YOUR_LOGIN_EMAIL";
    const password = "YOUR_LOGIN_PASSWORD";

    const browser = await puppeteer.connect({ browserWSEndpoint: 'wss://chrome.browserless.io?token='+token });
    const page = await browser.newPage();
    await page.setViewport({width:800,height:1020});
    await page.goto('https://cloud.browserless.io/account/',{ waitUntil: 'networkidle0'});
    await page.type('#login-email', email, {delay: 10});
    await page.type('#login-password', password, {delay: 10});
    await page.click('div.css-vxcmzt &amp;gt; div &amp;gt; button &amp;gt; span');
    await page.waitForSelector('.chartjs-render-monitor');
    await page.evaluate(() =&amp;gt; {
        var leftpanel = document.querySelector(".sticky_nav__3r2Ep");
        leftpanel.parentNode.removeChild(leftpanel);
        const date = new Date();
        document.querySelector('.text-white.mb-0').innerHTML="Sessions on "+date;
        document.querySelector('#app &amp;gt; div &amp;gt; div &amp;gt; div &amp;gt; .col-8').classList.add("col-12");
        document.querySelector('#app &amp;gt; div &amp;gt; div &amp;gt; div &amp;gt; .col-12').classList.remove("col-8");
    })
    await page.emulateMediaType('screen');
    return page.pdf({path:"dashboard.pdf",printBackground:true});
})();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a simple script, we basically access the dashboard URL, login with our credentials, and click on the sign in button. Once the dashboard is loaded, we generate the PDF.&lt;/p&gt;

&lt;p&gt;First we import the puppeteer core library, which is lightweight since you’ll be connecting to a remote or existing chrome, and doesn’t come with browser binaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const puppeteer = require('puppeteer-core');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll wrap all our code inside an async IIFE so that our code executes off the bat. Then we define our local variables, such as our API KEY, email, and password. The best practice here is to use process environments, but we’ll keep it simple for now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(async() =&amp;gt; {
    const token = "YOUR_API_KEY";
    const email = "YOUR_LOGIN_EMAIL";
    const password = "YOUR_LOGIN_PASSWORD";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s connect to the browserless WS endpoint by providing our API KEY and create a new browser and page to start automating.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const browser = await puppeteer.connect({ browserWSEndpoint: 'wss://chrome.browserless.io?token='+token });
const page = await browser.newPage();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that’s done, we’ll set the desired viewport&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.setViewport({
    width: 1920,
    height: 1080
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then go to the browserless account page and wait for the network traffic to settle down so that the email and password selectors are actually loaded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.goto('https://cloud.browserless.io/account/',
{ 
    waitUntil: 'networkidle0'
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We enter our credentials and click on the submit button – you can use environment variables for the password here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.type('#login-email', 'YOUR_EMAIL', {delay: 50});
await page.type('#login-password', 'YOUR_PASSWORD', {delay: 50});
await page.click('div.css-vxcmzt &amp;gt; div &amp;gt; button &amp;gt; span');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we’ve clicked log in, we want to wait to make sure the page is fully loaded by checking that the graph has been rendered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  await page.waitForSelector('.chartjs-render-monitor');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we want to modify the page before generating the PDF. We can do so inside the &lt;em&gt;page.evaluate()&lt;/em&gt; method. We are fetching the left panel navigation menu and removing it. Then we are finding the main panel that has the content that we want, we’ll remove the &lt;em&gt;.col-8&lt;/em&gt; class and add the &lt;em&gt;.col-12&lt;/em&gt; class so that it is fullscreen. You can feel free to modify the UI of your dashboard in this section, such as removing unwanted sections or adding new graphic elements by injecting html+css that you may want to show in the PDF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.evaluate(() =&amp;gt; {
  var leftpanel = document.querySelector(".sticky_nav__3r2Ep");
  leftpanel.parentNode.removeChild(leftpanel); //removing the left panel
  const date = new Date();
  document.querySelector('.text-white.mb-0').innerHTML="Sessions on "+date; //adding the date in the title
  document.querySelector('#app &amp;gt; div &amp;gt; div &amp;gt; div &amp;gt; .col-8').classList.add("col-12"); //adding this class to be fullscreen
  document.querySelector('#app &amp;gt; div &amp;gt; div &amp;gt; div &amp;gt; .col-12').classList.remove("col-8"); //removing this class to overwrite the container size.
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After all your modifications are done, feel free to generate the pdf. It is common that CSS defaults to print CSS styles (in order to save ink when printing) so you can add these two lines of code to make the CSS look more like a user would usually look at it. Otherwise the CSS could shift and look weird.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.emulateMediaType('screen'); //will help not to render print css
return page.pdf({printBackground:true}); //will render backgrounds of your page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In some cases pages are rendering their fonts with web fonts, so if your page looks weird even after adding these two lines of code, it could be that the web fonts aren’t loading properly because the page detects you’re running chrome headless, and hence doesn’t see the need to render any fonts at all. To overcome this, you can either run the session headful or set the user agent manually as so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.setUserAgent(
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s an example of why setting these last two lines of code are relevant:&lt;/p&gt;

&lt;p&gt;Page with only page.pdf(); looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nfwdWTVo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e06eqpzfiv7qnogdzu2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nfwdWTVo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e06eqpzfiv7qnogdzu2e.png" alt="Image description" width="880" height="827"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Page with print background looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wA7fM_wQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f83o8n8cz1c1f6rj80o3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wA7fM_wQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f83o8n8cz1c1f6rj80o3.png" alt="Image description" width="880" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;page with emulateMediaType set to screen looks like this (final result):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--474qSLjy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bs2eu40f8g52lvdovbr5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--474qSLjy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bs2eu40f8g52lvdovbr5.png" alt="Image description" width="880" height="829"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can take care of sending this PDF through your marketing platforms, have fun!&lt;/p&gt;

&lt;p&gt;👉 If you want to do this yourself, just create a &lt;a href="https://www.browserless.io/sign-up"&gt;Browserless account&lt;/a&gt; and get started!  &lt;/p&gt;

</description>
      <category>puppeteer</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
