<?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: Akshay Nathan</title>
    <description>The latest articles on DEV Community by Akshay Nathan (@akshaynathan).</description>
    <link>https://dev.to/akshaynathan</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%2Fuser%2Fprofile_image%2F232194%2Fbf6a1ab3-d3d8-475f-89fc-ac54a70c2b99.jpeg</url>
      <title>DEV Community: Akshay Nathan</title>
      <link>https://dev.to/akshaynathan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshaynathan"/>
    <language>en</language>
    <item>
      <title>Building a Typescript CLI</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Thu, 07 Nov 2019 16:51:52 +0000</pubDate>
      <link>https://dev.to/akshaynathan/building-a-typescript-cli-26h5</link>
      <guid>https://dev.to/akshaynathan/building-a-typescript-cli-26h5</guid>
      <description>&lt;h1&gt;
  
  
  Building a CLI with Typescript
&lt;/h1&gt;

&lt;p&gt;At &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt;, we're building a platform for end-to-end testing via a single API call. Our users give us a url and plain English instructions, and we use a human assisted trained model to verify each test-case. While one can use the walrus.ai API using &lt;strong&gt;curl&lt;/strong&gt; or any http library of their favorite language, we recently decided to build a command line tool to make it easier to submit walrus.ai tests, and plug them into existing CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;This blog post will go over building this CLI in Typescript. First, the finished product:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F53b8wudxnaq1z7cnntxp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F53b8wudxnaq1z7cnntxp.gif" alt="The walrus.ai CLI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up
&lt;/h2&gt;

&lt;p&gt;Let's create a new directory and initialize &lt;strong&gt;npm&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;cli
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;cli
&lt;span class="nv"&gt;$ &lt;/span&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We will need to install Typescript, the types for node, as well as &lt;a href="https://github.com/TypeStrong/ts-node" rel="noopener noreferrer"&gt;ts-node&lt;/a&gt; which will enable us to run Typescript files directly without compiling.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript @types/node ts-node


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Notice how we're installing all Typescript related packages as dev dependencies? This is because our published package will only need the compiled Javascript. More on that later.&lt;/p&gt;

&lt;p&gt;For now, let's create a basic &lt;a href="https://www.typescriptlang.org/docs/handbook/tsconfig-json.html" rel="noopener noreferrer"&gt;tsconfig.json&lt;/a&gt; for the Typescript compiler:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2017"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commonjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And now our first Typescript file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, we can compile and run this file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npx tsc
&lt;span class="nv"&gt;$ &lt;/span&gt;node dist/index.js
Hello World


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Remember &lt;strong&gt;ts-node&lt;/strong&gt;, which we installed earlier? We can use it to run our program more easily while developing. Let's create an npm script using &lt;strong&gt;ts-node&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// package.json&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scripts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ts-node src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npm run dev

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; npx ts-node src/index.ts

Hello World


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Accepting input
&lt;/h2&gt;

&lt;p&gt;Almost all command line tools follow similar flows — they accept input via arguments or stdin, they do something, and then they output results to stdout and errors to stderr.&lt;/p&gt;

&lt;p&gt;In node, a program's arguments are stored in an array inside &lt;code&gt;process.argv&lt;/code&gt;. You can access these arguments directly, or you can use an option parsing library to simplify access and create a better user experience. Some node options include &lt;a href="https://github.com/yargs/yargs" rel="noopener noreferrer"&gt;yargs&lt;/a&gt;, &lt;a href="https://github.com/tj/commander.js/" rel="noopener noreferrer"&gt;commander&lt;/a&gt;, and &lt;a href="https://github.com/nodeca/argparse" rel="noopener noreferrer"&gt;argparse&lt;/a&gt;. All three libraries have similar APIs, but we chose to go with yargs.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; API functionally takes in 3 required parameters. An &lt;strong&gt;API key&lt;/strong&gt; to identify the user, the &lt;strong&gt;url&lt;/strong&gt; of the application we want to test against, and a list of &lt;strong&gt;instructions&lt;/strong&gt; to execute and verify. Let's install yargs and parse these arguments.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

npm i yargs
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @types/yargs


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yargs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We can use the &lt;code&gt;demandOption&lt;/code&gt; parameter to require a program argument. If we try to rerun our script now, our program will complain about the missing arguments:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npm run dev

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; npx ts-node src/index.ts

Options:
  &lt;span class="nt"&gt;--help&lt;/span&gt;              Show &lt;span class="nb"&gt;help&lt;/span&gt;                                        &lt;span class="o"&gt;[&lt;/span&gt;boolean]
  &lt;span class="nt"&gt;--version&lt;/span&gt;           Show version number                              &lt;span class="o"&gt;[&lt;/span&gt;boolean]
  &lt;span class="nt"&gt;--api-key&lt;/span&gt;, &lt;span class="nt"&gt;-a&lt;/span&gt;                                              &lt;span class="o"&gt;[&lt;/span&gt;string] &lt;span class="o"&gt;[&lt;/span&gt;required]
  &lt;span class="nt"&gt;--url&lt;/span&gt;, &lt;span class="nt"&gt;-u&lt;/span&gt;                                                  &lt;span class="o"&gt;[&lt;/span&gt;string] &lt;span class="o"&gt;[&lt;/span&gt;required]
  &lt;span class="nt"&gt;--instructions&lt;/span&gt;, &lt;span class="nt"&gt;-i&lt;/span&gt;                                          &lt;span class="o"&gt;[&lt;/span&gt;array] &lt;span class="o"&gt;[&lt;/span&gt;required]

Missing required arguments: api-key, url, instructions


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;When we supply them, we can see that &lt;code&gt;yargs&lt;/code&gt; has parsed our arguments into a &lt;strong&gt;strongly-typed&lt;/strong&gt; map.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npm run dev &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'instruction'&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ts-node src/index.ts &lt;span class="s2"&gt;"-a"&lt;/span&gt; &lt;span class="s2"&gt;"key"&lt;/span&gt; &lt;span class="s2"&gt;"-u"&lt;/span&gt; &lt;span class="s2"&gt;"url"&lt;/span&gt; &lt;span class="s2"&gt;"-i"&lt;/span&gt; &lt;span class="s2"&gt;"instruction"&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  _: &lt;span class="o"&gt;[]&lt;/span&gt;,
  a: &lt;span class="s1"&gt;'key'&lt;/span&gt;,
  &lt;span class="s1"&gt;'api-key'&lt;/span&gt;: &lt;span class="s1"&gt;'key'&lt;/span&gt;,
  apiKey: &lt;span class="s1"&gt;'key'&lt;/span&gt;,
  u: &lt;span class="s1"&gt;'url'&lt;/span&gt;,
  url: &lt;span class="s1"&gt;'url'&lt;/span&gt;,
  i: &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'instruction'&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;,
  instructions: &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'instruction'&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;,
  &lt;span class="s1"&gt;'$0'&lt;/span&gt;: &lt;span class="s1"&gt;'src/index.ts'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Doing something
&lt;/h2&gt;

&lt;p&gt;Now that our CLI is accepting input, the next step is to &lt;strong&gt;do something&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the case of the &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; CLI, we want to call the API with our parsed arguments. Again, there are many libraries we can use to make HTTP requests including &lt;a href="https://github.com/visionmedia/superagent" rel="noopener noreferrer"&gt;superagent&lt;/a&gt;, &lt;a href="https://github.com/axios/axios" rel="noopener noreferrer"&gt;axios&lt;/a&gt;, and &lt;a href="https://github.com/request/request" rel="noopener noreferrer"&gt;request&lt;/a&gt;. In our case, we chose &lt;code&gt;axios&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

npm i axios


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yargs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;axios&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.walrus.ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Walrus-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Note that we're handling both branches of the Promise returned by &lt;code&gt;axios.post&lt;/code&gt;. Maintaining convention, we print successful results to &lt;code&gt;stdout&lt;/code&gt; and error messages to &lt;code&gt;stderr&lt;/code&gt;. Now when we run our program, it will silently wait while the test is completed, and then print out the results.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npm run dev &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; fake-key &lt;span class="nt"&gt;-u&lt;/span&gt; https://google.com &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'Search for something'&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cli@1.0.0 dev /Users/akshaynathan/dev/blog/cli
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ts-node src/index.ts &lt;span class="s2"&gt;"-a"&lt;/span&gt; &lt;span class="s2"&gt;"fake-key"&lt;/span&gt; &lt;span class="s2"&gt;"-u"&lt;/span&gt; &lt;span class="s2"&gt;"https://google.com"&lt;/span&gt; &lt;span class="s2"&gt;"-i"&lt;/span&gt; &lt;span class="s2"&gt;"Search for something"&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"error"&lt;/span&gt;: &lt;span class="s2"&gt;"Authentication required. Please sign in at https://app.walrus.ai/login."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Displaying progress
&lt;/h2&gt;

&lt;p&gt;We can improve on our CLI by making it slightly more interactive. On the web, long-running operations are often handled in the UI by displaying some sort of loading state. There are a few node libraries that can help us bring these UI paradigms to the command line.&lt;/p&gt;

&lt;p&gt;Loading bars are useful when the long-running task takes a relatively static amount of time, or if we have a discrete intuition about 'progress'. &lt;a href="https://github.com/visionmedia/node-progress" rel="noopener noreferrer"&gt;node-progress&lt;/a&gt; or &lt;a href="https://github.com/AndiDittrich/Node.CLI-Progress" rel="noopener noreferrer"&gt;cli-progress&lt;/a&gt; are both good libraries for this solution.&lt;/p&gt;

&lt;p&gt;In our case, however, while all &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; results are returned under 5 minutes, we don't have a discrete notion of progress. A test is either &lt;strong&gt;pending&lt;/strong&gt;, or it has been &lt;strong&gt;completed&lt;/strong&gt;. Spinners are a better fit for our CLI, and &lt;a href="https://github.com/sindresorhus/ora#readme" rel="noopener noreferrer"&gt;ora&lt;/a&gt; is a popular node spinner library.&lt;/p&gt;

&lt;p&gt;We can create our spinner before making our request, and clear our spinner once the Promise resolves or rejects.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yargs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ora&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ora&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spinner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ora&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Running test on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;axios&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.walrus.ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Walrus-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now when we run our program, we will see the spinner from the GIF above!&lt;/p&gt;

&lt;h2&gt;
  
  
  Exiting
&lt;/h2&gt;

&lt;p&gt;The last thing our CLI program has to do is to exit, and exit correctly. When programs exit, they can specify an integer exit code to indicate success of failure. Generally, any non-zero exit code indicates failure.&lt;/p&gt;

&lt;p&gt;For the &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; CLI, correctly specifying an exit code is imperative. Our users call our CLI from CI/CD pipelines. When a test fails, we have to exit with a non-zero exit code so that the next step in the pipeline, usually the deploy to production, doesn't run.&lt;/p&gt;

&lt;p&gt;You may be tempted to use node's &lt;code&gt;process.exit&lt;/code&gt; API:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;However, &lt;code&gt;process.exit&lt;/code&gt; will exit the program &lt;strong&gt;synchronously&lt;/strong&gt;, even if there are operations waiting to be run or caches that need to be flushed. The most common problem here is output. In the above code, depending on how our output is buffered, our program may exit &lt;strong&gt;before&lt;/strong&gt; our success or error messages are printed to the screen.&lt;/p&gt;

&lt;p&gt;We can solve this by simply setting the exit code, and letting the node script &lt;strong&gt;automatically&lt;/strong&gt; exit upon completion.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yargs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ora&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ora&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demandOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spinner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ora&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Running test on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;axios&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.walrus.ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Walrus-Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now when we run our script, it will fail with a non-zero exit code:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npm run dev &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; fake-key &lt;span class="nt"&gt;-u&lt;/span&gt; https://google.com &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'Search for something'&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ts-node src/index.ts &lt;span class="s2"&gt;"-a"&lt;/span&gt; &lt;span class="s2"&gt;"fake-key"&lt;/span&gt; &lt;span class="s2"&gt;"-u"&lt;/span&gt; &lt;span class="s2"&gt;"https://google.com"&lt;/span&gt; &lt;span class="s2"&gt;"-i"&lt;/span&gt; &lt;span class="s2"&gt;"Search for something"&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"error"&lt;/span&gt;: &lt;span class="s2"&gt;"Authentication required. Please sign in at https://app.walrus.ai/login."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
1


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Publishing
&lt;/h2&gt;

&lt;p&gt;Now that we've built our CLI, we need to publish it so our users can use it.&lt;/p&gt;

&lt;p&gt;There are many options we have here. Most simply, we can distribute the package and the CLI via npm. Alternatively, we could use a library like &lt;a href="https://github.com/zeit/pkg" rel="noopener noreferrer"&gt;pkg&lt;/a&gt; or &lt;a href="https://github.com/oclif/oclif" rel="noopener noreferrer"&gt;oclif&lt;/a&gt; to bundle &lt;code&gt;node&lt;/code&gt; itself into our binary. This way, users will not need to have npm or node installed to run our tool.&lt;/p&gt;

&lt;p&gt;Since &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; is a tool for running browser end-to-end tests, and our users are probably already familiar with npm and node, we decided to go with the simple option. First, we can edit our package.json to specify a binary, in this case &lt;code&gt;walrus&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@walrusai/cli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ts-node src/index.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"walrus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ISC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^12.12.6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/yargs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^13.0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ts-node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.4.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.7.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.19.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ora"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"yargs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^14.2.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, let's make our &lt;code&gt;index.ts&lt;/code&gt; runnable by telling the shell how to run it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;&lt;span class="cp"&gt;

#!/usr/bin/env node
&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we can use &lt;code&gt;npm link&lt;/code&gt;, to effectively link our node script into our path, as if we installed the binary.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npx tsc
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;link&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we can run our binary directly.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;walrus &lt;span class="nt"&gt;-a&lt;/span&gt; fake-key &lt;span class="nt"&gt;-u&lt;/span&gt; https://google.com &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'Search for something'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"error"&lt;/span&gt;: &lt;span class="s2"&gt;"Authentication required. Please sign in at https://app.walrus.ai/login."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;npm link&lt;/code&gt; is useful for development, but we want our users to be able to install our CLI more easily. For that, we can publish to &lt;code&gt;npm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, we should create a unique name for our package — &lt;code&gt;@walrusai/cli&lt;/code&gt; in our case.&lt;/p&gt;

&lt;p&gt;Next, we will need to create an account on &lt;a href="https://npmjs.com" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, authenticate in our command line, and then run:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;npx tsc
&lt;span class="nv"&gt;$ &lt;/span&gt;npm publish


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, our users can install our cli more easily:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @walrusai/cli&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Conclusion&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;In this blog post, we've built a Typescript CLI that accepts user input, makes an api call, outputs results, and exits correctly. You can check out the final open-source implementation of the &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; CLI &lt;a href="https://github.com/walrusai/cli" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Are you an engineer tired of building and maintaining flaky browser tests?  Try &lt;a href="http://walrus.ai" rel="noopener noreferrer"&gt;walrus.ai&lt;/a&gt; now, supply instructions in plain english, and receive results in under 5 minutes.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>cli</category>
    </item>
    <item>
      <title>Towards a fully programmable inbox</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Wed, 09 Oct 2019 18:20:57 +0000</pubDate>
      <link>https://dev.to/akshaynathan/programming-your-inbox-with-the-monolist-api-72i</link>
      <guid>https://dev.to/akshaynathan/programming-your-inbox-with-the-monolist-api-72i</guid>
      <description>&lt;p&gt;At &lt;a href="https://monolist.co"&gt;Monolist&lt;/a&gt;, we're building the command center for engineers. We integrate with all the tools engineers use (code hosting, project management, alerting), and aggregate all their tasks in one place.&lt;/p&gt;

&lt;p&gt;While we're constantly shipping new integrations, we understand that engineers may have specific workflows or internal tools that we can't natively integrate with. In this post, we'll go over using the new Monolist API to build a personalized workflow and get one step closer to automating your inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Let's say we have an app with mobile and web clients. Sometimes, because of bugs, these clients end up in request loops that flood our app servers and cause downtime. Now, obviously we could solve this problem at various points in the stack -- we could introduce throttling at the client level, at the load balancer level, or at the application level.&lt;/p&gt;

&lt;p&gt;For now, let's assume that we introduced some throttling at the application level using something like &lt;a href="https://github.com/kickstarter/rack-attack"&gt;this&lt;/a&gt; library for Rails, or &lt;a href="https://github.com/animir/node-rate-limiter-flexible"&gt;this&lt;/a&gt; one for Node.&lt;/p&gt;

&lt;p&gt;Once we've shipped these changes we will want to monitor their behavior. How many users are we throttling? What is the false positive rate? How does the added limiting improve service availability?&lt;/p&gt;

&lt;p&gt;Obviously, we could integrate our throttling directly into a more sophisticated monitoring system. We could (and should) build out dashboards that measure number of throttled users, and alerting for when excessive throttling takes place. However, this would require additional effort, and we may want to add this only after we've smoke tested the system. Is there a simpler solution?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Monolist
&lt;/h2&gt;

&lt;p&gt;Perhaps we simply want to be notified when a user is being throttled. We could send ourselves an email, or text message, but this notification is likely to be lost in the flood of emails/texts. On the flip side, we don't (yet) want to send an alert using something like PagerDuty, because we're not sure how frequently this will be happening, or what the correct severity should be.&lt;/p&gt;

&lt;p&gt;Instead, let's use the Monolist API to add a task to our Monolist every time a user is throttled.&lt;/p&gt;

&lt;p&gt;Note: The code samples in this blog post use Ruby and the HTTP library, but the Monolist API is accessible through any language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;push_to_monolist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.monolist.co/v1/action_items/new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="s2"&gt;"API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;action_item: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"Investigate throttled user &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;DESCRIPTION&lt;/span&gt;&lt;span class="sh"&gt;
          Links
            * [Mixpanel](https://mixpanel.com/users?user_id=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;)
            * [Logdna](https://logdna.com/logs?time=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;)
&lt;/span&gt;&lt;span class="no"&gt;        DESCRIPTION&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gWxCd5kn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/u8rvabujfnz00dwaza8f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gWxCd5kn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/u8rvabujfnz00dwaza8f.png" alt="Inbox API Item View"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we will be notified in Monolist along with our other tasks. We can prioritize and schedule this task the same as any other Monolist item. Because Monolist descriptions support Markdown, we have nicely formatted links that we can use to diagnose the issue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--av7FzWwu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3ot766fz5ouj41t0oqxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--av7FzWwu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3ot766fz5ouj41t0oqxm.png" alt="Expanded Item View"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One-click actions
&lt;/h2&gt;

&lt;p&gt;At this point, you may be thinking, "couldn't I just have sent myself an email?"&lt;/p&gt;

&lt;p&gt;The difference between Monolist and an email/text or any other type of notification, is that with the Monolist API, any task you create can be resolved directly in-line with fully customizable actions.&lt;/p&gt;

&lt;p&gt;Let's say that when we investigate a throttled user, we want to notify the ops on-call rotation for any users that are not false positives. This way, they will be able to have context on any downtime implications.&lt;/p&gt;

&lt;p&gt;With the Monolist API, we can add custom actions directly to our custom items. When a user takes a custom action, we can specify a destination url that Monolist will POST a payload to.&lt;/p&gt;

&lt;p&gt;Let's use the PagerDuty API to directly create an incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;push_to_monolist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.monolist.co/v1/action_items/new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="s2"&gt;"API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;action_item: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"Investigate throttled user &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;DESCRIPTION&lt;/span&gt;&lt;span class="sh"&gt;
          Links
            * [Mixpanel](https://mixpanel.com/users?user_id=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;)
            * [Logdna](https://logdna.com/logs?time=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;)
&lt;/span&gt;&lt;span class="no"&gt;        DESCRIPTION&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;actions: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Escalate to Ops"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="s2"&gt;"https://api.pagerduty.com/incidents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;headers: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="s2"&gt;"Authorization"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Token token=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;PAGERDUTY_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
          &lt;span class="ss"&gt;payload: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;incident: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"incident"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"User &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; throttled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="ss"&gt;service: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="no"&gt;PAGERDUTY_OPS_SERVICE_ID&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;When we create this new item, Monolist will encrypt and store the payload and headers objects. In the UI, the user will be able to take an action "Escalate to Ops"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8y4kM9Cr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/q7ri30ixz44vsp1ww05y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8y4kM9Cr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/q7ri30ixz44vsp1ww05y.png" alt="With Action Expanded View"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the user takes that action, Monolist will POST the payload to the PagerDuty API, automatically creating an incident for the Ops team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this post, we used the Monolist API to push a custom item with custom one-click actions into an user's Monolist.&lt;/p&gt;

&lt;p&gt;Interested in building your own integrations? Check out the docs &lt;a href="https://monolist.co/docs/api/v1"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>inbox</category>
      <category>ruby</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>Keep it simple stupid: Building full text search across all your tools with Postgres</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Thu, 03 Oct 2019 17:49:30 +0000</pubDate>
      <link>https://dev.to/akshaynathan/keep-it-simple-stupid-building-full-text-search-across-all-your-tools-with-postgres-27ho</link>
      <guid>https://dev.to/akshaynathan/keep-it-simple-stupid-building-full-text-search-across-all-your-tools-with-postgres-27ho</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fg79981a9wc9hs7t5tmkd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fg79981a9wc9hs7t5tmkd.gif" alt="Monolist Search"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the beginning there were tools, oh so many tools. There were code hosting tools, project management tools, messaging tools, alerting tools – an infinite set of tools. And for developers, all these tools contained tasks: code they had to review, bugs they needed to look at, questions to respond to, incidents to triage. Around a year ago, we asked "what if there was one tool where we could manage all our tasks from all our other tools", so we built it.&lt;/p&gt;

&lt;p&gt;As our customers started using &lt;a href="https://monolist.co" rel="noopener noreferrer"&gt;Monolist&lt;/a&gt;, we found that they were often asking a common question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I know that Sarah made a comment about the marketing page designs, but was that in the Jira task, or in the spec Google Doc, or on my pull request where I merged the designs, or on Slack?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course, each one of these tools have a search feature, but remembering where it is, or what syntax it supports is a massive pain. Monolist is already integrated with all these tools, and they all support search over their API, so we built a &lt;strong&gt;global search bar&lt;/strong&gt; in Monolist. It was pretty cool! It would debounce the user's input, search any items already in Monolist, call out to every API for every integrated tool in parallel, merge and deduplicate the results, and present them to the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But it was slow...&lt;/strong&gt; It was so slow, it took between 500ms and 5 seconds to deliver any results, which is unacceptable for a page load, let alone an autocomplete bar. You could already be typing "sea", and the autocomplete would only then finish the request for "s". What's more, it was so complex it was painful to debug, and painful to improve. We used a job system, Sidekiq, to make the API calls, Redis to store the result stream, and websockets to deliver them asynchronously to the user. The code was cool, but it was &lt;em&gt;overburdened with complexity&lt;/em&gt;, which led to a slow, buggy, and ultimately poor user experience.&lt;/p&gt;

&lt;p&gt;In this blog post, we'll walkthrough the foundation of our second attempt at search. We tried to follow one guiding principle from our first try, the age old cliche: &lt;strong&gt;KISS or Keep it simple, stupid.&lt;/strong&gt; While we're not big on the "stupid" part (&lt;strong&gt;KIS&lt;/strong&gt;), we've learned time and time again that complexity is the enemy, and that premature optimization, unnecessary abstractions, and cool technology for cool technology's sake are toxic to the end user experience. We'll use this principle throughout our journey, but first, let's look at the other objectives we had for the new experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objectives
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It needs to be fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an autocomplete experience to be effective, the results need to render quickly. We need the entire request, response, and render flow to take &amp;lt; 100ms, which is the largest amount of time an action can take while still being perceived as "instantaneous" to the user.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The results need to be relevant and (mostly) comprehensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We talked to our users and found that while they expected &lt;em&gt;breadth&lt;/em&gt; from the search on a frequent basis (they wanted results across all their tools), they didn't necessarily need &lt;em&gt;depth&lt;/em&gt; as often (they weren't frequently searching for terms in the body of an email, content of Google Doc, or description of an Asana Task).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;And, of course, &lt;strong&gt;KIS&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wanted to stay vigilant against complexity, which leads us to our next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about ElasticSearch?
&lt;/h2&gt;

&lt;p&gt;ElasticSearch is great. It's made for this purpose. It's unbelievably flexible. There are hosted solutions out there. But while it may have been easier to use ElasticSearch, and definitely easier to use a hosted search service like Algolia, &lt;a href="https://www.infoq.com/presentations/Simple-Made-Easy/" rel="noopener noreferrer"&gt;simplicity != easiness&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For one, it would introduce another system dependency to our architecture. In a self-hosted world, we would need to stand up, monitor, and maintain an ElasticSearch cluster. In a hosted world, this would be a little easier, but we'd still need to build in failsafes and retry semantics to get our data in and out of the search service.&lt;/p&gt;

&lt;p&gt;But even if we used a hosted service, we'd still need to think about data privacy. Our users trust us with sensitive business data, and we would have to guard against unauthorized access. We also take &lt;a href="https://monolist.co/blog/2019/09/delightful-goodbyes-guaranteeing-deletion/" rel="noopener noreferrer"&gt;account deletion very seriously&lt;/a&gt;, and if they deleted their accounts, this introduces another place we would have to ensure was properly wiped clean.&lt;/p&gt;

&lt;p&gt;What if there was a service that we had already setup to be highly available, already had sufficient monitoring in place, and already connected with our application and authorization system...&lt;/p&gt;

&lt;h2&gt;
  
  
  Just use the database (Postgres)
&lt;/h2&gt;

&lt;p&gt;Luckily, Postgres natively supports full text search and is plenty flexible for our use case. (Note: The code samples in this section are taken from our real Ruby (on Rails) code, but should be easily adapted to any language)&lt;/p&gt;

&lt;p&gt;Let's start with what we we're searching for. Monolist search supports multiple result types, including Google Drive files, contacts, and various types of tasks. For the sake of simplicity though, let's assume we only have two domain models, contacts and pull requests.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Contact&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Model&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:display_name&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:last_sent_at&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PullRequest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Model&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:user_id&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:repository_name&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These are some (adapted) examples from our own models. The fields are mostly self-explanatory. &lt;code&gt;Contact#last_sent_at&lt;/code&gt; is a denormalized timestamp of the last time this contact was the recepient or sender of an email for our user. Now let's make these objects searchable.&lt;/p&gt;

&lt;p&gt;There are two main database functions we will use for full text search in Postgres. First, &lt;code&gt;to_tsvector&lt;/code&gt; parses and tokenizes a string into &lt;em&gt;lexemes&lt;/em&gt;, which are basically words.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="n"&gt;monolist&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A quick brown fox jumped over a lazy dog'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                               &lt;span class="n"&gt;to_tsvector&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="s1"&gt;'brown'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="s1"&gt;'dog'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="s1"&gt;'fox'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="s1"&gt;'jumped'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="s1"&gt;'lazy'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="s1"&gt;'over'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="s1"&gt;'quick'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To query a &lt;code&gt;tsvector&lt;/code&gt;, we can use the &lt;code&gt;@@&lt;/code&gt; operand and a &lt;code&gt;tsquery&lt;/code&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="n"&gt;monolist&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'red'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A quick brown fox jumped over a lazy dog'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="k"&gt;column&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="c1"&gt;----------&lt;/span&gt;
 &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;monolist&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fox'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A quick brown fox jumped over a lazy dog'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="k"&gt;column&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="c1"&gt;----------&lt;/span&gt;
 &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Note, the first argument to both functions is a Postgres Full Text Search "config", which you can read more about &lt;a href="https://www.postgresql.org/docs/9.2/sql-createtsconfig.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but we won't be using for now.&lt;/p&gt;

&lt;p&gt;Simple right? We can just query for results directly on the attributes, something like &lt;code&gt;SELECT * FROM contacts WHERE to_tsquery('simple', 'QUERY') @@ to_tsvector('simple', 'display_name')&lt;/code&gt;. However, this only works for one table. If we have multiple tables (pull_requests), we'll have to outer join them as well. Let's add a new denormalized table.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TypeaheadResult&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Model&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:searchable&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:result_type&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:object_id&lt;/span&gt; &lt;span class="c1"&gt;# contact or pull request id&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:object_type&lt;/span&gt; &lt;span class="c1"&gt;# contact or pull request&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here, searchable refers to the indexed attribute (title for pull requests, or display_name for contacts).&lt;/p&gt;

&lt;p&gt;But wait, another table? Aren't we storing unnecessary data? &lt;strong&gt;KIS&lt;/strong&gt;: We don't have a storage problem yet, let's not prematurely optimize.&lt;/p&gt;

&lt;p&gt;One thing we can (maturely) optimize is based on a learning from our initial search. These tables are quite large (10 million + contacts, many more action items or google drive files). Even an indexed join takes up a significant portion of our 100ms allocation. Let's denormalize further and bring our whole base models into the TypeaheadResult. We can make the &lt;em&gt;searchable&lt;/em&gt; column json instead of text, and then write a service to populate it for a user.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PopulateTypeaheadResultsForUser&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;TypeheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;result_type: :contact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;searchable: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;display_name: &lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull_requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;TypeheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;result_type: :pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;searchable: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;repo: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Hold on, but what about our &lt;strong&gt;to_tsvector&lt;/strong&gt; function. We can't use that on JSON can we? Of course we can, it's Postgres.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

monolist=&amp;gt; SELECT to_tsquery('simple', 'cat') @@ to_tsvector('simple', '{ "name": "fox", "type": "canine" }'::jsonb);
 ?column?
----------
 f
(1 row)

monolist=&amp;gt; SELECT to_tsquery('simple', 'fox') @@ to_tsvector('simple', '{ "name": "fox", "type": "canine" }'::jsonb);
 ?column?
----------
 t
(1 row)


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let's write a service to actually retrieve the results for a user:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetTypeaheadResultsForUser&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="no"&gt;TypeaheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_sql&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;
        SELECT * FROM typeahead_results
          WHERE user_id = ? AND to_tsquery('simple', ?) @@ to_tsvector('simple', searchable),
      SQL
      user_id,
      query,
    ])
  end
end


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And finally, we can precompute the tsvectors and add an index to this column to make this query much, much faster.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;typeahead_results&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;searchable_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;typeahead_results&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;searchable_vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;searchable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;typeahead_results&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;searchable_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Great! We have a comprehensive and simple solution. However, we still have no notion of relevancy. If a user types in "a", the first result may be "&lt;a href="mailto:alice@monolist.co"&gt;alice@monolist.co&lt;/a&gt;", or it may be "&lt;a href="mailto:analytics-marketing-campaign@marketing.google.com"&gt;analytics-marketing-campaign@marketing.google.com&lt;/a&gt;'.&lt;/p&gt;

&lt;p&gt;Postgres actually has many solutions for relevancy, mainly around the handy &lt;code&gt;ts_rank&lt;/code&gt; function. We could define a relevancy metric based on which indexed field we matched (repository name should be less relevant than title for a pull request), the length of the match, and the type of the result...&lt;/p&gt;

&lt;p&gt;Or, we can &lt;strong&gt;KIS&lt;/strong&gt;. From talking more to our users, we decided that (last_interacted_with || updated_at || created_at) was a good enough proxy for relevancy. In other words, take the items the user has interacted with most recently, or have been updated or created most recently first. For contacts, we can use &lt;code&gt;last_sent_at&lt;/code&gt;, and for pull requests, we can use &lt;code&gt;created at&lt;/code&gt;. After adding a new column to &lt;code&gt;typeahead_results&lt;/code&gt; our new services look like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PopulateTypeaheadResultsForUser&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;TypeheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;result_type: :contact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;searchable: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;display_name: &lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="ss"&gt;last_referenced_at: &lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_sent_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull_requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;TypeheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;result_type: :pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;searchable: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;repo: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="ss"&gt;last_referenced_at: &lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetTypeaheadResultsForUser&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="no"&gt;TypeaheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_sql&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;
        SELECT * FROM typeahead_results
          WHERE user_id = ? AND to_tsquery('simple', ?) @@ searchable_vector
          ORDER BY last_referenced_at DESC
      SQL
      user_id,
      query,
    ])
  end
end


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There's still one more problem. Now that we're ordering results, the most frequent results will push out the others. For the query, "a", there may be tons of pull requests in the "analytics" repository that flood out the "&lt;a href="mailto:alice@monolist.co"&gt;alice@monolist.co&lt;/a&gt;" contact who sent us an email last week. We could implement some sort of intelligent scheme, where depending on query length we priortize one result type or another, or...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KIS&lt;/strong&gt; We could also just limit results by type:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetTypeaheadResultsForUser&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="no"&gt;TypeaheadResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_sql&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;
        SELECT * FROM typeahead_results
          WHERE user_id = ? AND to_tsquery('simple', ?) @@ searchable_vector
          ORDER BY last_referenced_at DESC
      SQL
      user_id,
      query,
    ]).group_by(&amp;amp;:result_type)
      .map { |k, v| [k, v.take(5)]}
      .values
      .flatten
  end
end


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And now, the moment you (and we) are all waiting for. How does this relatively simple solution perform?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IT FLIES&lt;/strong&gt; Even with millions and millions of typeahead_results, the indexed query with no joins gives us results around 100ms. With some clever React work (which we'll talk about in a subsequent post), we were able to get the entire request-&amp;gt;response-&amp;gt;render median time to just under 100ms (97ms).&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs and future
&lt;/h2&gt;

&lt;p&gt;Obviously, there are tradeoffs in this solution. For one, the denormalized table requires us to be very careful about updates and deletions. Luckily, Postgres supports indices on nested JSON keys, so we can index &lt;strong&gt;contact.email&lt;/strong&gt; for example, and then just delete and reinsert all results when a specific contact is updated.&lt;/p&gt;

&lt;p&gt;Another large tradeoff is flexibility. ElasticSearch is obviously more powerful than Postgres, and from a scaling perspective has a better story. We can horizontally scale ES instances separately from our database, since our search index may grow relatively independently of live data volume.&lt;/p&gt;

&lt;p&gt;Lastly, our search may be faster, but is also more shallow. We're no longer searching email or drive file content, but we haven't found this to be problematic for our users, since the "See all results" link still takes the user to a (slower) deep search experience when necessary.&lt;/p&gt;

&lt;p&gt;All in all, the response to our search improvements has been unanimously positive, and we think these tradeoffs were necessary decisions in our pursuit of a fast, simple, and maintainable solution.&lt;/p&gt;




&lt;p&gt;Want to see for yourself? Tired of going to 5 different tools to search for the "Product Spec"? Try Monolist for free &lt;a href="https://monolist.co" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Enjoyed this post? Join our mailing list &lt;a href="http://eepurl.com/gFxG3n" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>postgres</category>
      <category>rails</category>
      <category>search</category>
    </item>
    <item>
      <title>Ruby 2.7 Experimental Features in Production: Pattern matching and numbered block args</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Tue, 01 Oct 2019 18:12:55 +0000</pubDate>
      <link>https://dev.to/akshaynathan/ruby-2-7-experimental-features-in-production-pattern-matching-and-numbered-block-args-jcg</link>
      <guid>https://dev.to/akshaynathan/ruby-2-7-experimental-features-in-production-pattern-matching-and-numbered-block-args-jcg</guid>
      <description>&lt;p&gt;At &lt;a href="https://monolist.co"&gt;Monolist&lt;/a&gt;, we're building the command center for engineers. We integrate with all the tools engineers use (code hosting, project management, alerting),&lt;br&gt;
and aggregate all their tasks in one place. If you've read our &lt;a href="https://monolist.co/blog/2019/08/fail-fast-and-fail-often/"&gt;previous&lt;/a&gt;&lt;br&gt;
&lt;a href="https://monolist.co/blog/2019/09/getting-started-sorbet/"&gt;blog&lt;/a&gt; &lt;a href="https://monolist.co/blog/2019/09/delightful-goodbyes-guaranteeing-deletion/"&gt;posts&lt;/a&gt;, you know that most of our backend services are built using Ruby.&lt;/p&gt;

&lt;p&gt;The creator of Ruby, Matz, once said that "the goal of ruby is to make programmers happy." We share this goal at Monolist. Ruby enables us to iterate quickly to deliver new integrations&lt;br&gt;
and features to our customers, programmers, faster. We are especially excited about &lt;a href="https://www.ruby-lang.org/en/news/2019/05/30/ruby-2-7-0-preview1-released/"&gt;Ruby 2.7&lt;/a&gt;, and we're already running some of our less critical services on the preview build released earlier this summer.&lt;/p&gt;

&lt;p&gt;In this blog post, we'll talk about our favorite new (experimental) features of Ruby 2.7 in the few months we've been running it in production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern Matching
&lt;/h2&gt;

&lt;p&gt;Pattern Matching is one of Ruby 2.7's marquee features. Here's a simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;integration: :github&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Fix onboarding bug"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="ss"&gt;integration: :github&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:
  &lt;/span&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Found github pull_request: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="ss"&gt;integration: :asana&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:
  &lt;/span&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Found asana task: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Found other item"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="c1"&gt;# =&amp;gt; Found github pull request: Fix onboarding bug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Pattern matching in Ruby is a little bit different than in other languages. In statically typed languages like Rust or Haskell, pattern matching constructs enable the compiler to guarantee that they're exhaustive. In other words, code that doesn't handle every possible case won't compile. In Ruby, if the pattern is not exhaustive, you'll instead get an error at runtime, &lt;code&gt;NoMatchingPattern&lt;/code&gt;. This isn't as useful, but still better than a potentially hidden bug were we to use a conditional.&lt;/p&gt;

&lt;p&gt;We've found instead that pattern matching's most useful quality in Ruby is for destructuring deeply nested hashes. Consider the following (real but simplified) code we use to get a pull request's last updated timestamp. If the pull request doesn't have any completed build statuses, we use the time the pull request was created. Otherwise, we use the timestamp from the build status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pull_request_updated_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:statuses&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"success"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"failure"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now let's rewrite this code using pattern matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pull_request_updated_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;
  &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;statuses: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Notice that with pattern matching, we can eliminate some of the nested conditionals and safe navigation from our first attempt. Additionally, the pattern matching version is easier to read, and easier to reason about. The patterns explicitly connote our assumptions about the shape of our data, assumptions that are otherwise hidden behind if statements.&lt;/p&gt;

&lt;p&gt;Since Monolist relies heavily on third party API's, we find ourselves often dealing with complex, nested data structures. Pattern matching has helped us be more explicit and deliberate in our handling of this data, reducing bugs, and making it easier for the next person to add functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbered block arguments
&lt;/h2&gt;

&lt;p&gt;At Monolist, we're heavy users of blocks/procs and lambdas. We especially love Ruby's fluent api with Enumerables, which enables us to write clean and readable code like follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get relevant pull requests for user by repository&lt;/span&gt;
&lt;span class="n"&gt;pull_requests&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assignees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merged_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closed_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It's easy to tell what this code is doing. We're filtering pull requests by assignee, then selecting only the open ones, removing any that the user created, and finally grouping by repository id.&lt;/p&gt;

&lt;p&gt;However, one annoyance we faced before was the constant need for arbitrary block param names in simple blocks. It's obvious that the &lt;code&gt;s&lt;/code&gt; in each block is referring to a pull request, but we still have to repeat our name every time. This pattern littered our codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  monolist ag &lt;span class="nt"&gt;--stats&lt;/span&gt; &lt;span class="s2"&gt;"{ &lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | ag &lt;span class="s2"&gt;" matches"&lt;/span&gt;
219 matches
133 files contained matches
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This has been solved in Ruby 2.7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get relevant pull requests for user by repository&lt;/span&gt;
&lt;span class="n"&gt;pull_requests&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assignees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merged_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closed_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Instead of using random characters (s, t, p) all over our codebase, we can reliably refer to block params by number. We think that this improves readability, and the general developer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monolist ❤️ Ruby
&lt;/h2&gt;

&lt;p&gt;At Monolist, we're incredibly excited for the stable release of Ruby 2.7 later this year, Ruby 3 in the coming years, and beyond. For more Ruby content, check out our first foray into adding types with Sorbet &lt;a href="https://monolist.co/blog/2019/09/getting-started-sorbet/"&gt;here&lt;/a&gt;, or the patterns we use to scale our API integrations &lt;a href="https://monolist.co/blog/2019/08/fail-fast-and-fail-often/"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Are you a Ruby engineer that sometimes spends more time scanning tools for tasks and pull requests, than actually writing code? Monolist can help! Sign up for free &lt;a href="https://monolist.co"&gt;here&lt;/a&gt;.
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Liked this post? Join our mailing list for more content &lt;a href="http://eepurl.com/gFxG3n"&gt;here&lt;/a&gt;.
&lt;/h3&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>patterns</category>
      <category>features</category>
    </item>
    <item>
      <title>Delightful Goodbyes: Fully Deleting User Data</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Mon, 30 Sep 2019 21:05:37 +0000</pubDate>
      <link>https://dev.to/akshaynathan/delightful-goodbyes-fully-deleting-user-data-2hea</link>
      <guid>https://dev.to/akshaynathan/delightful-goodbyes-fully-deleting-user-data-2hea</guid>
      <description>&lt;p&gt;At &lt;a href="https://monolist.co"&gt;Monolist&lt;/a&gt;, we’re building the command center for engineers. We integrate with all the tools an engineer uses, aggregate their tasks, and provide intelligence to make sure that nothing slips through the cracks.&lt;/p&gt;

&lt;p&gt;Unfortunately, like any company, we sometimes have to say goodbye to one of our users. Creating a delightful experience throughout the product is core to our company culture, and we believe that account deletion should be no exception. While it may seem counterproductive to focus on this user story, we think it’s a dark pattern to make deletion difficult. It frustrates me when it’s hard to delete my account from certain services, and we don’t want our users to feel the same way.&lt;/p&gt;

&lt;p&gt;However, because Monolist is built on integrations with different tools, we rely on a complex data model that makes deleting all of a user’s data, fully and efficiently, rather difficult.&lt;/p&gt;

&lt;p&gt;In this post we’ll focus on ensuring that a user’s data is fully deleted, which is important for compliance with GDPR and other regulations, but is also just the &lt;em&gt;right thing to do&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting Simple
&lt;/h2&gt;

&lt;p&gt;In the beginning, our user deletion service was as simple as possible. (Note: The code snippets in this blog post are written in Rails and ActiveRecord, but the behavior and message should be the same regardless of language/framework/ORM).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeleteUser&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;destroy!&lt;/code&gt; method in ActiveRecord deletes a record from the database and executes all relevant callbacks. Rails provides an easy way to handle associations as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:action_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;dependent: :destroy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This ensures that when a user is destroyed, their action items will be destroyed as well.&lt;/p&gt;

&lt;p&gt;The problem with this approach, however, is that it requires us to be very careful about adding “dependent: :destroy” to our associations. Let’s take a look at the action item model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActionItem&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="ss"&gt;:properties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;dependent: :destroy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Do you see the problem here? When we call &lt;code&gt;user.destroy!&lt;/code&gt; now, we will correctly destroy all action items and their properties, but we won’t destroy notifications! This means that we’re still storing user data, even after they’ve deleted their account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Testing
&lt;/h2&gt;

&lt;p&gt;An obvious solution to this problem is to use foreign keys. Foreign keys enable referential integrity, and ensure that an associated database record (Action Item), cannot be deleted while it’s associations (Notifications and Properties) are still present.&lt;/p&gt;

&lt;p&gt;However, note that foreign keys still depend on us to actually create them! If we forget to create a foreign key, and forget to add &lt;code&gt;{ dependent: :destroy }&lt;/code&gt; to an association, we’re back at square one.&lt;/p&gt;

&lt;p&gt;We quickly realized that we needed a comprehensive way to test deletion. We needed our test to be self-updating -- that is, the efficacy of the test shouldn’t rely on the engineer adding an association to update it. Here’s a simplified version of what we came up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"DeleteUser"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:exclusions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:blacklisted_ips&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:marketing_email_types&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;action_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActionItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="no"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;action_item: &lt;/span&gt;&lt;span class="n"&gt;action_item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="no"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;action_item: &lt;/span&gt;&lt;span class="n"&gt;action_item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"should exhaustively delete user"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;exclusions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT count(*) from &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT count(*) from &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let’s walk through this test line by line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;exclusions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;First, we retrieve the full list of tables in our database. We exclude some tables from our test. Exclusions are tables that aren’t associated with a specific user. For example, we have a dynamic list of IP addresses that we blacklist, which will not be mutated when a user deletes their account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT count(*) from &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next we verify that our test is set up properly. We make sure that any table we expect to have data, has data. This step is extremely important. Consider an engineer building a new feature. If they add a new model, the tests will fail in CI, since their new table will have no entries. Only then are they forced to remember to update the DeleteUser test, and update the association to be properly destroyed.&lt;/p&gt;

&lt;p&gt;Next, we call our service: &lt;code&gt;DeleteUser.new.call(user)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT count(*) from &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Finally, we assert that all our tables are empty, which implies that our test user has been properly deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While this test may seem simple, we’ve found that it’s an effective way for us to guarantee that we’re fully deleting our user’s data upon their request. When we run into false positives, we add tables to the exclusions list. This opt-in vs opt-out approach forces our engineering team to consider the implications of every database association we add, while still iterating quickly.&lt;/p&gt;

&lt;p&gt;However, this is only one facet of building a delightful deletion experience. In our next post in the series, we’ll talk about our learnings in deletion performance, and how we optimized our user deletion interaction from minutes to seconds.&lt;/p&gt;




&lt;h3&gt;
  
  
  Tired of refreshing Github and Jira for notifications? &lt;a href="https://monolist.co"&gt;Try Monolist&lt;/a&gt; and aggregate all your tasks in one place!
&lt;/h3&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>deletion</category>
      <category>data</category>
    </item>
    <item>
      <title>Getting Started with the Sorbet Type Checker in Rails</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Wed, 25 Sep 2019 17:13:57 +0000</pubDate>
      <link>https://dev.to/akshaynathan/getting-started-with-the-sorbet-type-checker-in-rails-2nmj</link>
      <guid>https://dev.to/akshaynathan/getting-started-with-the-sorbet-type-checker-in-rails-2nmj</guid>
      <description>&lt;p&gt;At &lt;a href="https://monolist.co"&gt;Monolist&lt;/a&gt;, we’re building a command center for engineers. We integrate with the APIs of all the tools engineers commonly use to aggregate their tasks in one place. For example, our customers trust us to pull in their Jira issues, Github pull requests, and Pagerduty alerts in real-time, so that they can triage, prioritize, and complete their work all from within Monolist.&lt;/p&gt;

&lt;p&gt;Unfortunately, this is easier said than done. The APIs of these SaaS tools vary wildly, and assumptions about responses and return values propagate through our Ruby on Rails backend. When these assumptions are incorrect, because of Ruby’s dynamic nature, they manifest as runtime errors that cause issues for our users, and are difficult to debug. We realized quickly that we could benefit from types. While our frontend and mobile clients are written in Typescript, we didn’t have a good solution for our large rails API.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://sorbet.run"&gt;Sorbet&lt;/a&gt;. Sorbet is a ruby typechecker written by Stripe. Like Typescript, Sorbet allows for gradual typing, which enables us to slowly introduce types to our codebase. In other words, we can reap the safety benefits of adding types file by file.&lt;/p&gt;

&lt;p&gt;In this blog series, we’ll detail our experiences with adding Sorbet to a large-ish (&amp;gt; 100k LoC) Rails codebase. In this first post, we’ll add sorbet to the project, and add types to our first file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;We’ll start by adding Sorbet and the Sorbet runtime to our Gemfile. Sorbet provides both static analysis, and runtime checks. While the runtime checks will run in production, we’ll only need the static analysis in development, so we can separate the gems by environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"sorbet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;group: :development&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"sorbet-runtime"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;According to the Sorbet documentation, at this point we can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Unfortunately, here’s where we hit our first hurdle – the command never completed. The "srb init" command requires every &lt;code&gt;.rb&lt;/code&gt; file in the repository, and uses runtime reflection to detect missing constants and generate RBI type definition files for your existing code. If you have a large number of vendored dependencies in &lt;code&gt;vendor/bundle/&lt;/code&gt; or &lt;code&gt;node_modules/&lt;/code&gt;, this can be an extremely long (and sometimes infinite) process.&lt;/p&gt;

&lt;p&gt;There are a &lt;a href="https://github.com/sorbet/sorbet/issues/975"&gt;couple&lt;/a&gt; &lt;a href="https://github.com/sorbet/sorbet/issues/1259"&gt;open&lt;/a&gt; Github issues about this problem. Luckily, the help output from &lt;code&gt;srb init&lt;/code&gt; gives us an easy way to solve this problem. We can  simply add &lt;code&gt;# typed: ignore&lt;/code&gt; to all the ruby files in our &lt;code&gt;vendor/&lt;/code&gt; and &lt;code&gt;node_modules/&lt;/code&gt; directories, and Sorbet will skip them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;rb &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;find vendor node_modules &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.rb'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'1i\# typed: ignore'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rb&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now when we run &lt;code&gt;srb init&lt;/code&gt;, the command completes. We can now run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
No errors! Great job.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Great!&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning on the type-checker
&lt;/h2&gt;

&lt;p&gt;The good news is we have no errors. The bad news is that we’re not actually type-checking most of our code.&lt;br&gt;
This is because when &lt;code&gt;srb init&lt;/code&gt; cannot typecheck an entire file due to missing method type signatures,&lt;br&gt;
it will add &lt;code&gt;# typed: false&lt;/code&gt; or &lt;code&gt;# typed: ignore&lt;/code&gt; to the file.&lt;br&gt;
This means that &lt;code&gt;srb tc&lt;/code&gt; will not typecheck the types and methods defined in these files or their callers.&lt;/p&gt;

&lt;p&gt;Let’s take a look at one of our files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# typed: false&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateStripeBillingCustomer&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;stripe_token: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/test\+\d*@monolist.co/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;stripe_customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;source: &lt;/span&gt;&lt;span class="n"&gt;stripe_token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="no"&gt;BillingCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stripeid: &lt;/span&gt;&lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This service is responsible for initializing a user with Stripe, our billing platform.&lt;br&gt;
For a non-test user, it calls the Stripe API to initialize a customer, and stores the customer_id in our database.&lt;/p&gt;

&lt;p&gt;Billing code is especially sensitive – you don’t want to double charge a customer because of an unexpected null object.&lt;br&gt;
We figured that our billing code would be a good place to start adding Sorbet types, and this service is an especially good candidate because of its simplicity.&lt;/p&gt;

&lt;p&gt;Let’s change &lt;code&gt;# typed: false&lt;/code&gt; to &lt;code&gt;# typed: true&lt;/code&gt;.&lt;br&gt;
In &lt;code&gt;# typed: true&lt;/code&gt; mode, Sorbet only checks types for methods that it knows about. That is, Sorbet&lt;br&gt;
only checks methods that are explicitly annotated, either via hand-written type signatures or type definition RBI files.&lt;br&gt;
For any other methods, Sorbet assumes their return values are &lt;code&gt;T.untyped&lt;/code&gt;, which is a special type that matches all types.&lt;br&gt;
While &lt;code&gt;# typed: true&lt;/code&gt; isn’t the strictest static type checking level, we can start with it to enable us to gradually type our codebase.&lt;/p&gt;

&lt;p&gt;Once we’ve changed the annotation, we can rerun &lt;code&gt;srb tc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
app/services/billing/create_stripe_billing_customer.rb:6: Method create does not exist on T.class_of&lt;span class="o"&gt;(&lt;/span&gt;Stripe::Customer&lt;span class="o"&gt;)&lt;/span&gt; https://srb.help/7003
     6 |    stripe_customer &lt;span class="o"&gt;=&lt;/span&gt; Stripe::Customer.create&lt;span class="o"&gt;({&lt;/span&gt;
     7 |      name: user.display_name,
     8 |      email: user.email,
     9 |      &lt;span class="nb"&gt;source&lt;/span&gt;: stripe_token,
    10 |    &lt;span class="o"&gt;})&lt;/span&gt;
  Autocorrect: Use &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to autocorrect
    app/services/billing/create_stripe_billing_customer.rb:6: Replace with freeze
     6 |    stripe_customer &lt;span class="o"&gt;=&lt;/span&gt; Stripe::Customer.create&lt;span class="o"&gt;({&lt;/span&gt;
                                               ^^^^^^

Errors: 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Hm, it appears that Sorbet doesn’t seem to know about the &lt;code&gt;Stripe::Customer.create&lt;/code&gt; method.&lt;br&gt;
In Sorbet, all methods must either include an inline type signature via a “sig”, which we’ll see later, or must include type signatures in a corresponding “.rbi” file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding type signatures
&lt;/h2&gt;

&lt;p&gt;Sorbet ships with .rbi files for the Ruby standard library. All other type definition files must either be hand-written, or pulled down from sorbet/sorbet-typed, a central repository of types for common gems.&lt;/p&gt;

&lt;p&gt;When we run srb init, sorbet will automatically pull these type definitions for any gems installed in our gem file. Let’s see what it did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  &lt;span class="nb"&gt;ls &lt;/span&gt;sorbet/rbi/sorbet-typed/lib
actionmailer  activemodel   activesupport railties      sidekiq
actionpack    activerecord  bundler       rainbow       stripe
actionview    activestorage minitest      ruby
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;At first, it looks like we pulled down all the definitions for the “stripe” gem. However, upon closer inspection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  &lt;span class="nb"&gt;cat &lt;/span&gt;sorbet/rbi/sorbet-typed/lib/stripe/all/stripe.rbi
&lt;span class="c"&gt;# This file is autogenerated. Do not edit it by hand. Regenerate it with:&lt;/span&gt;
&lt;span class="c"&gt;#   srb rbi sorbet-typed&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# If you would like to make changes to this file, great! Please upstream any changes you make here:&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#   https://github.com/sorbet/sorbet-typed/edit/master/lib/stripe/all/stripe.rbi&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# typed: strong&lt;/span&gt;

class Stripe::Card
  sig &lt;span class="o"&gt;{&lt;/span&gt; returns&lt;span class="o"&gt;(&lt;/span&gt;String&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def brand&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; params&lt;span class="o"&gt;(&lt;/span&gt;other: String&lt;span class="o"&gt;)&lt;/span&gt;.returns&lt;span class="o"&gt;(&lt;/span&gt;String&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def &lt;span class="nv"&gt;brand&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;other&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; returns&lt;span class="o"&gt;(&lt;/span&gt;Integer&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def exp_month&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; params&lt;span class="o"&gt;(&lt;/span&gt;other: Integer&lt;span class="o"&gt;)&lt;/span&gt;.returns&lt;span class="o"&gt;(&lt;/span&gt;Integer&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def &lt;span class="nv"&gt;exp_month&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;other&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; returns&lt;span class="o"&gt;(&lt;/span&gt;Integer&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def exp_year&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; params&lt;span class="o"&gt;(&lt;/span&gt;other: Integer&lt;span class="o"&gt;)&lt;/span&gt;.returns&lt;span class="o"&gt;(&lt;/span&gt;Integer&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def &lt;span class="nv"&gt;exp_year&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;other&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; returns&lt;span class="o"&gt;(&lt;/span&gt;String&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def last4&lt;span class="p"&gt;;&lt;/span&gt; end

  sig &lt;span class="o"&gt;{&lt;/span&gt; params&lt;span class="o"&gt;(&lt;/span&gt;other: String&lt;span class="o"&gt;)&lt;/span&gt;.returns&lt;span class="o"&gt;(&lt;/span&gt;String&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  def &lt;span class="nv"&gt;last4&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;other&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; end
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It looks like the “sorbet-typed” definitions for the stripe gem are really sparse. This was new to us after learning to lean so heavily on DefinitelyTyped, which is the Typescript equivalent of a shared type library.&lt;/p&gt;

&lt;p&gt;However, this is easily solvable. Let’s add a .rbi file of our own at "sorbet/rbi/lib/stripe/customer.rb".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stripe::Customer&lt;/span&gt;
  &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nilable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;source: &lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nilable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:);&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here we define the &lt;code&gt;create&lt;/code&gt; method in accordance to the Stripe API documentation.&lt;br&gt;
Our sig states that the method takes in an optional String email, and an optional String source, and returns an &lt;code&gt;Stripe::Customer&lt;/code&gt;.&lt;br&gt;
Let’s run &lt;code&gt;srb tc&lt;/code&gt; again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
app/services/billing/create_stripe_billing_customer.rb:8: Method &lt;span class="nb"&gt;id &lt;/span&gt;does not exist on Stripe::Customer https://srb.help/7003
     8 |    BillingCustomer.create!&lt;span class="o"&gt;({&lt;/span&gt; user: user, stripeid: stripe_customer.id &lt;span class="o"&gt;})&lt;/span&gt;
                                                            ^^^^^^^^^^^^^^^^^^
Errors: 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let’s add the &lt;code&gt;id&lt;/code&gt; method to our &lt;code&gt;Stripe::Customer&lt;/code&gt; type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And now…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
No errors! Great job.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Stronger Guarantees
&lt;/h2&gt;

&lt;p&gt;Remember when we said that &lt;code&gt;# typed: true&lt;/code&gt; isn’t the strictest type-checking mode? Now that we’ve resolved the errors with that level of strictness, let’s see if we can go further with the aptly named &lt;code&gt;# typed: strict&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once we changed the annotation, we can rerun &lt;code&gt;srb tc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
app/services/billing/create_stripe_billing_customer.rb:3: This &lt;span class="k"&gt;function &lt;/span&gt;does not have a &lt;span class="sb"&gt;`&lt;/span&gt;sig&lt;span class="sb"&gt;`&lt;/span&gt; https://srb.help/7017
     3 |  def call&lt;span class="o"&gt;(&lt;/span&gt;user:, stripe_token: nil&lt;span class="o"&gt;)&lt;/span&gt;
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Autocorrect: Use &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to autocorrect
    app/services/billing/create_stripe_billing_customer.rb:3: Insert sig &lt;span class="o"&gt;{&lt;/span&gt;params&lt;span class="o"&gt;(&lt;/span&gt;user: T.untyped, stripe_token: T.untyped&lt;span class="o"&gt;)&lt;/span&gt;.returns&lt;span class="o"&gt;(&lt;/span&gt;T.untyped&lt;span class="o"&gt;)}&lt;/span&gt;

     3 |  def call&lt;span class="o"&gt;(&lt;/span&gt;user:, stripe_token: nil&lt;span class="o"&gt;)&lt;/span&gt;
          ^
  Autocorrect: Use &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to autocorrect
    app/services/billing/create_stripe_billing_customer.rb:3: Insert   extend T::Sig

     3 |  def call&lt;span class="o"&gt;(&lt;/span&gt;user:, stripe_token: nil&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In strict mode, Sorbet mandates that all methods must have a signature. Let’s add one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;stripe_token: &lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nilable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nilable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BillingCustomer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;stripe_token: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now when we rerun srb tc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
app/services/billing/create_stripe_billing_customer.rb:12: Method email does not exist on User https://srb.help/7003
    12 |    &lt;span class="k"&gt;return if &lt;/span&gt;user.email.match?&lt;span class="o"&gt;(&lt;/span&gt;/demo&lt;span class="se"&gt;\+\d&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;@monolist.co/&lt;span class="o"&gt;)&lt;/span&gt;
                      ^^^^^^^^^^
  Autocorrect: Use &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to autocorrect
    app/services/billing/create_stripe_billing_customer.rb:12: Replace with &lt;span class="nb"&gt;eval
    &lt;/span&gt;12 |    &lt;span class="k"&gt;return if &lt;/span&gt;user.email.match?&lt;span class="o"&gt;(&lt;/span&gt;/demo&lt;span class="se"&gt;\+\d&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;@monolist.co/&lt;span class="o"&gt;)&lt;/span&gt;
                           ^^^^^

app/services/billing/create_stripe_billing_customer.rb:14: Method email does not exist on User https://srb.help/7003
    14 |    stripe_customer &lt;span class="o"&gt;=&lt;/span&gt; Stripe::Customer.create&lt;span class="o"&gt;({&lt;/span&gt; email: user.email, &lt;span class="nb"&gt;source&lt;/span&gt;: stripe_token &lt;span class="o"&gt;})&lt;/span&gt;
                                                               ^^^^^^^^^^
  Autocorrect: Use &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to autocorrect
    app/services/billing/create_stripe_billing_customer.rb:14: Replace with &lt;span class="nb"&gt;eval
    &lt;/span&gt;14 |    stripe_customer &lt;span class="o"&gt;=&lt;/span&gt; Stripe::Customer.create&lt;span class="o"&gt;({&lt;/span&gt; email: user.email, &lt;span class="nb"&gt;source&lt;/span&gt;: stripe_token &lt;span class="o"&gt;})&lt;/span&gt;
                                                                    ^^^^^

Errors: 2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;user.email&lt;/code&gt; method is dynamically generated by ActiveRecord from the database schema. Unfortunately, Sorbet does not know about dynamically generated methods, and adding .rbi definitions for every column among our 100+ models would be incredibly tedious.&lt;br&gt;
Luckily, the open-source project &lt;a href="https://github.com/chanzuckerberg/sorbet-rails"&gt;"sorbet-rails"&lt;/a&gt; allows us to autogenerate .rbi files for our models.&lt;/p&gt;

&lt;p&gt;Once we install it with &lt;code&gt;gem "sorbet-rails"&lt;/code&gt;, we can run &lt;code&gt;bundle exec rake rails_rbi:models&lt;/code&gt; to generate .rbi files for every model in our project. Let’s rerun &lt;code&gt;srb tc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
app/services/billing/create_stripe_billing_customer.rb:12: Method match? does not exist on NilClass component of T.nilable&lt;span class="o"&gt;(&lt;/span&gt;String&lt;span class="o"&gt;)&lt;/span&gt; https://srb.help/7003
    12 |    &lt;span class="k"&gt;return if &lt;/span&gt;user.email.match?&lt;span class="o"&gt;(&lt;/span&gt;/demo&lt;span class="se"&gt;\+\d&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;@monolist.co/&lt;span class="o"&gt;)&lt;/span&gt;
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Autocorrect: Use &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to autocorrect
    app/services/billing/create_stripe_billing_customer.rb:12: Replace with T.must&lt;span class="o"&gt;(&lt;/span&gt;user.email&lt;span class="o"&gt;)&lt;/span&gt;
    12 |    &lt;span class="k"&gt;return if &lt;/span&gt;user.email.match?&lt;span class="o"&gt;(&lt;/span&gt;/demo&lt;span class="se"&gt;\+\d&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;@monolist.co/&lt;span class="o"&gt;)&lt;/span&gt;
                      ^^^^^^^^^^
Errors: 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Whoa! Our first real type error! Let’s look into the generated signature in "sorbet/rails-rbi/models/user.rbi"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nilable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Looks like sorbet-rails assigned &lt;code&gt;T.nilable&lt;/code&gt; to the return type of &lt;code&gt;User#email&lt;/code&gt;. This is because the email column is nullable in our database. This is a legacy remnant from when email address was not required to sign up for Monolist. For these legacy users, we should simply ignore them and not attempt to create a customer on Stripe.&lt;/p&gt;

&lt;p&gt;Let’s implement this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;stripe_token: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/demo\+\d*@monolist.co/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;stripe_customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;source: &lt;/span&gt;&lt;span class="n"&gt;stripe_token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="no"&gt;BillingCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stripeid: &lt;/span&gt;&lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now, when we rerun &lt;code&gt;srb tc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;srb tc
No errors! Great job.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here, we can see the value of Sorbet’s “flow-sensitivity”. This means that Sorbet tracks types through program control flow. Although &lt;code&gt;user.email&lt;/code&gt; starts as &lt;code&gt;T.nilable(String)&lt;/code&gt;, the conditional check on line 1 unwraps email to &lt;code&gt;String&lt;/code&gt;, fixing the type error from before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this blog post, we setup our Rails API project with Sorbet, added type definitions for our models and some of our external dependencies, and fully typed our first file. While Sorbet still has some rough edges (inflexible initialization process, small type library), we were still able to relatively easily enable it’s static type-checking and discover our first real type error.&lt;/p&gt;

&lt;p&gt;At Monolist, we believe that adding types is a great way to ensure the stability of the API integrations that our customers depend on. In the next posts in this series, we’ll integrate Sorbet into our CI system, enable runtime checking, and start adding types to more complicated code paths.&lt;/p&gt;




&lt;h3&gt;
  
  
  Want to see how Monolist can enable engineers like you to be more productive? Sign up for free &lt;a href="https://monolist.co"&gt;here&lt;/a&gt;.
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Liked this post? Join our mailing list for more content &lt;a href="http://eepurl.com/gFxG3n"&gt;here&lt;/a&gt;.
&lt;/h3&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>sorbet</category>
      <category>typechecking</category>
    </item>
    <item>
      <title>Fail Fast and Fail Often: Handling API Errors at Scale</title>
      <dc:creator>Akshay Nathan</dc:creator>
      <pubDate>Wed, 18 Sep 2019 18:19:04 +0000</pubDate>
      <link>https://dev.to/akshaynathan/fail-fast-and-fail-often-handling-api-errors-at-scale-17mb</link>
      <guid>https://dev.to/akshaynathan/fail-fast-and-fail-often-handling-api-errors-at-scale-17mb</guid>
      <description>&lt;p&gt;At &lt;a href="https://monolist.co" rel="noopener noreferrer"&gt;Monolist&lt;/a&gt;, we’re building the software engineer’s ideal inbox. Our users depend on us to surface all relevant and actionable tasks and context across all the tools and services they use. For a typical engineer, this includes emails, outstanding Slack messages, Github pull requests, and Jira issues or Asana tasks.&lt;/p&gt;

&lt;p&gt;To do this, we make millions of API requests each day, spread over a dozen different services. These requests are made via ruby code running on multiple Sidekiq job processing containers across many hosts. (The following post uses Ruby and Sidekiq as examples, but the learnings are relevant to any language and job processing framework.)&lt;/p&gt;

&lt;p&gt;As these requests succeed, much more processing has to occur to decide which data is relevant to the user, and how to present it to them. In the process of building Monolist, we’ve learned a lot about how to do this scalably, which is probably a good topic for another blog post. In this post, however, I want to share our learnings about what to do with the 100,000 requests that fail each day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry early but retry correctly
&lt;/h2&gt;

&lt;p&gt;When working with APIs, we generally see two classes of errors: Ephemeral errors are errors that may not occur when repeating the same request some time in the future. In contrast, persistent errors will consistently happen on each subsequent request. A connection timeout, for example, is an ephemeral error which will likely be resolved on retry. A 401 error, however, is probably indicative that a user is not authenticated and will not get automatically resolved.&lt;/p&gt;

&lt;p&gt;Generally, persistent errors require us to change our business logic to make successful requests, while ephemeral errors will succeed on the next retry with no intervention.&lt;/p&gt;

&lt;p&gt;At Monolist, we rely on Sidekiq’s default retry semantics for both classes of errors. For persistent errors, we deploy code fixes and let the next retry succeed. We see around 10 of these errors daily, and the failure rate decreases as our new integrations become more stable. The rest of our daily 100,000 errors are all ephemeral. While these errors usually resolve within 1 or 2 retries, we’ve had to carefully architect our systems to handle this failure volume successfully.&lt;/p&gt;

&lt;p&gt;We believe in failing fast and often, rather than letting failed state propagate down our code paths with unintended consequences. As such, we raise exceptions for unexpected api responses, and avoid catching them in our workers.&lt;/p&gt;

&lt;p&gt;Below is a simplified snippet of real code that we use to synchronize our users’ inboxes with Gitplace. Given a user and an api client, we poll Gitplace for the user’s pull requests, and create Monolist action items for each one we find.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_gitplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_request_comments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;create_action_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that if the api call at line 3 fails, Sidekiq will automatically retry the entire job, and hopefully it will succeed the next time. But is there anything wrong with this code?&lt;/p&gt;

&lt;p&gt;The answer lies on line 5. Let’s say the job made it through 4 pull requests the first time, before the comments api call failed. Because Sidekiq will naively retry the entire worker, we’ll duplicate all 4 of the already created action items the next retry!&lt;/p&gt;

&lt;p&gt;Obviously, this is not preferable. When relying on retries to solve ephemeral errors, it’s imperative that our jobs are idempotent. An idempotent job has no additional side effects when successfully rerun. In other words, we should be able to run our jobs N times, and expect the same results regardless of N. Let’s fix that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_gitplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_request_comments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gitplace_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;create_action_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when our job is rerun, we will only create action items that haven’t already been created. This means Sidekiq can run our job however many times it wants, and the end result for our users will still be correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Always make progress
&lt;/h2&gt;

&lt;p&gt;While we’ve fixed one bug, there’s another more silent and dangerous issue with the code above. Let’s say we have a user with 1000’s of pull requests. If we hit an ephemeral error, say a network timeout, while retrieving the comments for the 999th pull request, when the job retries we will start all over and make another 1000 api calls.&lt;/p&gt;

&lt;p&gt;Even more problematic, even if our ephemeral error rate is low, an increased number of api calls increases our chance of hitting an ephemeral error. Thus, the jobs that are slowest and take the most resources, are not only the most likely to be retried again, but they’re the least likely to succeed on each subsequent retry! Even with Sidekiq’s automatic exponential backoff, this can debilitate our queue’s when we have hundreds or even thousands of expensive jobs failing and retrying together.&lt;/p&gt;

&lt;p&gt;The solution to this problem is usually API specific, but follows a common principle: always track and make progress in your expensive jobs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_gitplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gitplace_last_sync&lt;/span&gt;

  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_requests&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;created_after: &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_request_comments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;github_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;create_action_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;gitplace_last_sync: &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above code sample, we store the last synced pull request created_at field. When polling Gitplace, we only retrieve pull requests created_after the last synced request. We then make sure that stored progress tracker gets updated regardless of whether there is an exception. That way, even if our job retries, we’ll only make the necessary api requests to continue.&lt;/p&gt;

&lt;p&gt;Note: This approach does have concurrency implications that are outside the scope of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track your failures
&lt;/h2&gt;

&lt;p&gt;While our code can now handle all the ephemeral errors we can throw at it, our monitoring systems are a different story. At Monolist, we use Sentry for exception tracking and alerting, but the following strategies are applicable regardless of how your team handles exceptions.&lt;/p&gt;

&lt;p&gt;Obviously, we can’t have 100,000 irrelevant exceptions flooding our Sentry daily. However, we can’t catch the exceptions in code because we still need them to propagate up to Sidekiq so that the jobs are retried. We also don’t want to blindly ignore these exceptions -- while each incremental ephemeral error is usually just noise, we want to know if the rate of ephemeral errors changes significantly, to alert us to some larger problem with our integration, network, or service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;track_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hincrby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tracked_exceptions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Monolist&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TrackedException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_gitplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gitplace_last_sync&lt;/span&gt;

  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_requests&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;created_after: &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pull_request_comments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;github_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;create_action_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Gitplace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ConnectionTimeout&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="n"&gt;track_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;gitplace_last_sync: &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the end, we came up with a simple solution where we explicitly track each ephemeral exception that we’re aware of. We wrap the exceptions in a Monolist::TrackedException wrapper which we’ve added to our Sentry blacklist so we don’t see them in Sentry. Since Monolist::TrackedException is still an exception, Sidekiq will still retry the job as usual. At the same time, we increment a counter in Redis to keep track of the number of exceptions we’ve seen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F77zl031dpf57zdcj5zhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F77zl031dpf57zdcj5zhs.png" alt="Grafana"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our monitoring systems, we’ve surfaced the “tracked_exceptions” key in Redis to our Prometheus instance. This allows us to add graphs like the one above to our dashboards, and alerting when the rate of exceptions changes significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While the APIs we interface with at Monolist vary wildly, we’ve consistently found that the quality of our integrations depends heavily on how resilient we are to api errors, and how much of that complexity we can hide from the end user.&lt;/p&gt;

&lt;p&gt;By abstracting away retry behavior, ensuring that jobs are idempotent, and making sure that we’re always getting closer to success, our end users are fully oblivious to the errors, and can focus on staying productive, writing code, and being the best they can be at their jobs.&lt;/p&gt;

&lt;p&gt;Don’t believe me? &lt;a href="https://monolist.co" rel="noopener noreferrer"&gt;Try out Monolist for yourself.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Liked this post? Join our mailing list for more content &lt;a href="http://eepurl.com/gFxG3n" rel="noopener noreferrer"&gt;here&lt;/a&gt;.
&lt;/h3&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>sidekiq</category>
      <category>api</category>
    </item>
  </channel>
</rss>
