<?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: Potloc</title>
    <description>The latest articles on DEV Community by Potloc (@potloc).</description>
    <link>https://dev.to/potloc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3824%2Fa8f4745d-fb55-4b09-8f0f-ba8fbbdec271.png</url>
      <title>DEV Community: Potloc</title>
      <link>https://dev.to/potloc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/potloc"/>
    <language>en</language>
    <item>
      <title>How to safely rename STI models in Rails</title>
      <dc:creator>Jérôme Parent-Lévesque</dc:creator>
      <pubDate>Fri, 18 Aug 2023 18:39:32 +0000</pubDate>
      <link>https://dev.to/potloc/how-to-safely-rename-sti-models-in-rails-25lf</link>
      <guid>https://dev.to/potloc/how-to-safely-rename-sti-models-in-rails-25lf</guid>
      <description>&lt;p&gt;In Rails, Single Table Inheritance (STI) models store  their full model name (including any module namespaces) in a &lt;code&gt;type&lt;/code&gt; column. This column is used by ActiveRecord to determine which model to instantiate when loading a record from the database. This means that renaming such models isn't as easy as just changing the class name; it must also involve a data migration to update the values stored as &lt;code&gt;type&lt;/code&gt;. However, how can we safely perform this in a live, production environment?&lt;/p&gt;




&lt;p&gt;This is a challenge that we recently ran into at Potloc while working on modularization of our codebase. This involved namespacing all of our models under &lt;a href="https://github.com/rubyatscale/packs-rails"&gt;packs&lt;/a&gt;, which meant that STI models's &lt;code&gt;type&lt;/code&gt; values also had to be updated.&lt;/p&gt;

&lt;p&gt;Shopify Engineering posted last year &lt;a href="https://shopify.engineering/changing-polymorphic-type-rails"&gt;a blog post&lt;/a&gt; about this same issue (albeit for Polymorphic models) in which they suggest to change entirely the nature of what is stored as &lt;code&gt;type&lt;/code&gt; in the database. However, they mention that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our solution adds complexity. It’s probably not worth it for most use cases&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And this was indeed how we felt for our use case. We wanted to perform this in a way that would have &lt;strong&gt;no impact&lt;/strong&gt; on the way Rails works, and all while having zero downtime.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Solution
&lt;/h1&gt;

&lt;p&gt;Let's jump right in to the final solution for those who don't need all the details and just want a quick step-by-step guide!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In a first deployment;

&lt;ul&gt;
&lt;li&gt;Rename the model to whatever you need&lt;/li&gt;
&lt;li&gt;Create, using the old model name, a new model &lt;em&gt;that inherits from the renamed model&lt;/em&gt; but that is otherwise empty&lt;/li&gt;
&lt;li&gt;Remove all uses of the old model in the codebase&lt;/li&gt;
&lt;li&gt;Make sure that everywhere the &lt;code&gt;type&lt;/code&gt; name was being used (whether as a raw string or through &lt;a href="https://api.rubyonrails.org/v7.0.1/classes/ActiveRecord/Inheritance/ClassMethods.html#method-i-sti_name"&gt;&lt;code&gt;#sti_name&lt;/code&gt;&lt;/a&gt;), both the new and old type name are now supported&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Migrate the data in the &lt;code&gt;type&lt;/code&gt; column of all database records to reflect the new model name&lt;/li&gt;
&lt;li&gt;In a final deployment, remove the deprecated classes and old &lt;code&gt;type&lt;/code&gt; names used in the codebase&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Renaming the model
&lt;/h2&gt;

&lt;p&gt;To help navigating through these steps, let's use a simple example:&lt;br&gt;
Your team is currently modularizing the codebase and wants to create a new pack for their aerospace 🚀 division. You are therefore tasked to move an STI model named &lt;code&gt;Rocket&lt;/code&gt; (say this model is under a base &lt;code&gt;Vehicle&lt;/code&gt; model and &lt;code&gt;vehicles&lt;/code&gt; database table) into a new namespace: &lt;code&gt;Aerospace::Rocket&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can start by renaming the model directly:&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="c1"&gt;# models/aerospace/rocket.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Aerospace&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rocket&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Vehicle&lt;/span&gt;
    &lt;span class="c1"&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;Then, here comes the neat trick: We will create a sub-type of &lt;code&gt;Aerospace::Rocket&lt;/code&gt; using the old model name:&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="c1"&gt;# models/rocket.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rocket&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Aerospace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rocket&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;Notice that this model is completely empty. In fact, we shouldn't use it &lt;em&gt;anywhere&lt;/em&gt; in the codebase (except for its &lt;code&gt;#sti_name&lt;/code&gt;, we'll come back to that later).&lt;/p&gt;

&lt;p&gt;This is not by accident. It turns out that ActiveRecord, under the hood, will use the &lt;code&gt;sti_name&lt;/code&gt; of the current model, &lt;strong&gt;as well as the &lt;code&gt;sti_name&lt;/code&gt; of any child models&lt;/strong&gt; when querying records!&lt;br&gt;
This means that by making the old model name inherit from the new one, we get for free the following behaviour:&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="no"&gt;Aerospace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; SELECT * FROM vehicles WHERE type IN ('Aerospace::Rocket', 'Rocket');&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will therefore pave the way for us to then run a data migration that changes all &lt;code&gt;Rocket&lt;/code&gt; types stored in the database to &lt;code&gt;Aerospace::Rocket&lt;/code&gt; without breaking anything! 🎉&lt;br&gt;
But before we do that, we have to take care of a couple more cases.&lt;/p&gt;

&lt;p&gt;First, we want all new records created to use the new type name. This simply means replacing all uses of &lt;code&gt;Rocket&lt;/code&gt; by &lt;code&gt;Aerospace::Rocket&lt;/code&gt; in the codebase.&lt;/p&gt;

&lt;p&gt;Second, if this model's &lt;code&gt;#sti_name&lt;/code&gt; or its raw string ("Rocket") were used anywhere (for example in active record queries) we now have to make sure to support both the new and the old names.&lt;br&gt;
In a typical ActiveRecord query, this might look something like this:&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="c1"&gt;# From:&lt;/span&gt;
&lt;span class="n"&gt;fleet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vehicles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sti_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# To:&lt;/span&gt;
&lt;span class="n"&gt;fleet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vehicles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Aerospace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sti_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sti_name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# Or, better yet:&lt;/span&gt;
&lt;span class="no"&gt;Aerospace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;fleets: &lt;/span&gt;&lt;span class="n"&gt;fleet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, there might be other instances in your code where you might be using the &lt;code&gt;#sti_name&lt;/code&gt; in a different way. You'll need to individually take a look at each of these. For example, since at Potloc we are using GraphQL and have some &lt;code&gt;Enum&lt;/code&gt; types defined for STI models, we had to make sure that both possible &lt;code&gt;type&lt;/code&gt; values would coerce to the same enum value that is sent back from the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The data migration
&lt;/h2&gt;

&lt;p&gt;That was the hard part! After step 1 is deployed, the rest is pretty much just business-as-usual when working in a continuous deployment environment.&lt;/p&gt;

&lt;p&gt;In this step, we need to rename all old type names stored in the database to the new one. We can achieve this with a data migration (a good guide for this is the &lt;a href="https://github.com/ankane/strong_migrations#backfilling-data"&gt;strong-migrations gem readme&lt;/a&gt;).&lt;br&gt;
Note that this step may vary depending on your team's choice of how to run data migrations, but no matter the approach the following command (or equivalent) needs to be run in the production environment:&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="no"&gt;Vehicle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sti_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="no"&gt;Aerospace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sti_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Cleanup
&lt;/h2&gt;

&lt;p&gt;We should now be at a point where no records in the database are using the old &lt;code&gt;sti_name&lt;/code&gt; anymore and any newly created records are all stored using the new name as &lt;code&gt;type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can therefore cleanup everything!&lt;/p&gt;

&lt;p&gt;First, we can remove the old &lt;code&gt;Rocket&lt;/code&gt; model (the one that was  empty and inherited from &lt;code&gt;Aerospace::Rocket&lt;/code&gt;).&lt;br&gt;
And finally, we can remove any special logic we added in Step 1 to support both &lt;code&gt;Rocket.sti_name&lt;/code&gt; and &lt;code&gt;Aerospace::Rocket.sti_name&lt;/code&gt; to now only support the latter.&lt;/p&gt;

&lt;p&gt;And that's it! Migration complete! 🔥&lt;/p&gt;

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

&lt;p&gt;It took a few steps, but by leveraging Rails' mechanism that fetches database records matching any of a model's children &lt;code&gt;#sti_name&lt;/code&gt;s, we were able to rename our &lt;code&gt;Rocket&lt;/code&gt; model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;without any downtime, and;&lt;/li&gt;
&lt;li&gt;without any changes to Rails' handling of STI models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, although this blog post didn't cover it, a similar process can also be used for renaming models used in Polymorphic associations. This might be the subject of a future article.&lt;/p&gt;

&lt;p&gt;Hopefully this guide can help you to easily rename STI models, especially when it comes to modularization of your large Rails monoliths (something we can strongly recommend after a few months of trying &lt;a href="https://github.com/rubyatscale"&gt;&lt;code&gt;packs-rails&lt;/code&gt;&lt;/a&gt; internally)!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interested in what we do at Potloc? Come join us! &lt;a href="https://jobs.lever.co/Potloc?team=Engineering"&gt;We are hiring&lt;/a&gt; 🚀&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Data Analytics at Potloc I: Making data integrity your priority with Elementary &amp; Meltano</title>
      <dc:creator>Stéphane Burwash</dc:creator>
      <pubDate>Fri, 06 Jan 2023 03:28:33 +0000</pubDate>
      <link>https://dev.to/potloc/data-analytics-at-potloc-i-making-data-integrity-your-priority-with-elementary-meltano-1ob</link>
      <guid>https://dev.to/potloc/data-analytics-at-potloc-i-making-data-integrity-your-priority-with-elementary-meltano-1ob</guid>
      <description>&lt;h3&gt;
  
  
  Foreword
&lt;/h3&gt;

&lt;p&gt;This is the first of a series of small blog posts where we describe plugins that our data engineering team at &lt;a href="https://www.potloc.com/" rel="noopener noreferrer"&gt;Potloc&lt;/a&gt; developed in order to solve business issues that we were facing.&lt;/p&gt;

&lt;p&gt;These features were developed to enhance our current stack, which consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://meltano.com/" rel="noopener noreferrer"&gt;Meltano&lt;/a&gt; as our DataOps platform + Extract / Load tool&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.getdbt.com/" rel="noopener noreferrer"&gt;dbt&lt;/a&gt; as our data transformation tool&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/airflow" rel="noopener noreferrer"&gt;Airflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/bigquery" rel="noopener noreferrer"&gt;Bigquery&lt;/a&gt; as our data warehouse&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/fargate/" rel="noopener noreferrer"&gt;AWS Fargate&lt;/a&gt; as our hosting infrastructure, managed through &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;terraform&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, it is assumed that the reader has basic knowledge of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meltano&lt;/li&gt;
&lt;li&gt;dbt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We hope that you enjoy the article! If you have any questions, feel free to reach out.&lt;/p&gt;

&lt;p&gt;This article was not sponsored by Elementary in any way, we're just big fans 😉.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data Integrity - It's more than a buzzword
&lt;/h1&gt;

&lt;p&gt;Data integrity is an essential component of a data pipeline. Without integrity and trust, your pipeline is essentially worthless, and those hundreds of hours you spent integrating new sources, optimizing load times, and modeling raw data into usable insights go down the drain. More than once, our data team has created "production ready" dashboards, only to realise that the integrity / buisness logic behind the dashboard was completely flawed. What was supposed to be a 2 day project became a 3 week debacle.  &lt;/p&gt;

&lt;p&gt;To circumvent falling into the data quality trap, you can write tests using a number of powerful open-source data integrity solutions such as &lt;a href="https://greatexpectations.io/" rel="noopener noreferrer"&gt;Great Expectations&lt;/a&gt;, &lt;a href="https://www.soda.io/core" rel="noopener noreferrer"&gt;Soda Core&lt;/a&gt; or even natively using &lt;a href="https://docs.getdbt.com/docs/build/tests" rel="noopener noreferrer"&gt;dbt tests&lt;/a&gt; to validate that your data is doing what it's supposed to. But as you write more and more tests, you can start running into some issues, mainly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracking over time&lt;/strong&gt;: How do you keep track of test results over time? How are you progressing in terms of tackling these issues?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This was the starting point for our quest to find a long-term data integrity solution at Potloc. We were attempting to map integrity issues in user-inputted data. We also wanted to give our team an accurate report of their progress as they resolved these issues one-by-one in the source data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Readability&lt;/strong&gt;: As you go from 5 to 50 to 500 to 5000+ tests, reading results becomes exponentially more complicated and time-intensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;: Once you have detected an integrity issue, how do you quickly reproduce the test to be able to investigate?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown unknowns&lt;/strong&gt;: While it is possible to test for every known possible issue, it can be harder / impossible to test for unknown unknowns such as dataset shifts, anomalies, large spikes in row count, etc.&lt;/p&gt;

&lt;p&gt;These issues can be circumvented by integrating &lt;a href="https://www.elementary-data.com/" rel="noopener noreferrer"&gt;Elementary&lt;/a&gt; into your workflow.&lt;/p&gt;

&lt;h1&gt;
  
  
  Features
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Elementary&lt;/strong&gt; is an open-source tool for data observability and validation that wraps around an existing dbt project. It allows users to graduate from simply &lt;strong&gt;having&lt;/strong&gt; integrity tests to &lt;strong&gt;using&lt;/strong&gt; them in order to improve confidence in your data product.&lt;/p&gt;

&lt;p&gt;Here are only some of the reasons why I personally love Elementary:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The UI, the glorious UI:
&lt;/h3&gt;

&lt;p&gt;Elementary and its associated CLI (command line interface) &lt;em&gt;edr&lt;/em&gt; natively allow you to &lt;strong&gt;generate a static HTML file containing test results&lt;/strong&gt;. This file can either be viewed locally, sent through slack or even hosted in a cloud service to be viewed as a webpage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqhtb932ag6r0b5xpqy7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqhtb932ag6r0b5xpqy7.png" alt="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pqhtb932ag6r0b5xpqy7.png" width="800" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this UI, you can view your most recent test results, historical test results, check model run times and even view lineage graphs.&lt;/p&gt;

&lt;p&gt;If you have any failures in tests run, you can view samples of offending entries or copy the SQL query that generated these errors to quickly investigate.&lt;/p&gt;

&lt;p&gt;You can play around with Elementary's &lt;a href="https://storage.googleapis.com/elementary_static/elementary_demo.html#/test-results" rel="noopener noreferrer"&gt;demo project&lt;/a&gt; to get a feel for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Stored results and historical views
&lt;/h3&gt;

&lt;p&gt;Elementary integrates with your dbt project in order to store all test runs and uses &lt;em&gt;on-run-end&lt;/em&gt; hooks to upload results. This all happens on the dbt package, without need to connect to the data warehouse.&lt;/p&gt;

&lt;p&gt;This allows us to view test results over time, view progress from run to run, and &lt;strong&gt;use test results for internal reporting&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It can sometimes be hard to share integrity metrics with the rest of your non-data-literate team. Having easy access to results &amp;amp; metrics such as row count directly in your warehouse allows you to create integrity dashboards curated for business use cases, giving your team the opportunity to start tackling issues and take stock of their progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Anomaly detection tests
&lt;/h3&gt;

&lt;p&gt;As mentioned above, it is hard to deal with unknown unknowns and issues that arise over longer periods of time (days, weeks, or even months). Even if your data is clean when your model first goes into production, it does not mean that mistakes/issues cannot slip in as time goes on. A supported table requires constant monitoring, especially if business logic has been hard-coded.&lt;/p&gt;

&lt;p&gt;This task can be greatly alleviated by making use of Elementary's native &lt;strong&gt;anomaly detection tests&lt;/strong&gt;, which monitor for shifts at the table and column level for metrics such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row count&lt;/li&gt;
&lt;li&gt;Null count&lt;/li&gt;
&lt;li&gt;Percentage of missing&lt;/li&gt;
&lt;li&gt;Freshness&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A full list of all anomaly metrics Elementary tests for can be found &lt;a href="https://docs.elementary-data.com/guides/add-elementary-tests" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By basing itself on &lt;strong&gt;past results&lt;/strong&gt; rather than hard-coded baselines (ex: an increase of 10% in row count or half-day delay in freshness), Elementary can easily be integrated out-of-the-box without needing to fine-tune from pipeline to pipeline.&lt;/p&gt;

&lt;p&gt;At Potloc, we mainly use this feature to identify &lt;strong&gt;freshness issues&lt;/strong&gt;. Elementary allows you to easily setup freshness checks &lt;em&gt;without having to specify hard deadlines&lt;/em&gt; (ex: 12h since last sync, 24h since last updated, etc.). This means that we can change our upstream Extract/Load job schedules without having to change our downstream tests; the tool will automatically flag the issue, and then adapt to the new schedule as it becomes norm. This also makes the intial setup is quick and painless.&lt;/p&gt;

&lt;h1&gt;
  
  
  Integrating Elementary into your existing Meltano Project
&lt;/h1&gt;

&lt;p&gt;We developed an &lt;a href="https://hub.meltano.com/utilities/elementary/" rel="noopener noreferrer"&gt;Elementary plugin&lt;/a&gt; for Meltano using the Meltano EDK that can easily be integrated into your project.&lt;/p&gt;

&lt;p&gt;To add it, simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meltano add utiliy elementary

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

&lt;/div&gt;



&lt;p&gt;This should add the following code snippet to your &lt;code&gt;meltano.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  - name: elementary
    variant: elementary
    pip_url: elementary-data==&amp;lt;EDR VERSION&amp;gt; git+https://github.com/potloc/elementary-ext.git

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

&lt;/div&gt;



&lt;p&gt;You will also need to add the following snippet to your &lt;code&gt;packages.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages:
  - package: elementary-data/elementary
    version: &amp;lt;DBT PACKAGE VERSION&amp;gt;
    ## Docs: &amp;lt;https://docs.elementary-data.com&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;As you can see, we have 2 elements we now need to complete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EDR version&lt;/li&gt;
&lt;li&gt;dbt package version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of the can be found in the elementary &lt;a href="https://docs.elementary-data.com/quickstart" rel="noopener noreferrer"&gt;quickstart documentation&lt;/a&gt; or in their respective package indexes (&lt;a href="https://pypi.org/project/elementary-data/" rel="noopener noreferrer"&gt;pypi&lt;/a&gt; and &lt;a href="https://hub.getdbt.com/elementary-data/elementary/latest/" rel="noopener noreferrer"&gt;dbt packages&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It is important that &lt;strong&gt;both of these versions are aligned in accordance with creator releases&lt;/strong&gt;. If CLI and dbt package versions are misaligned, errors can ensue.&lt;/p&gt;

&lt;p&gt;At the time of writing this article, we would be using &lt;code&gt;EDR VERSION = 0.63&lt;/code&gt; &amp;amp; &lt;code&gt;DBT PACKAGE VERSION = 0.66&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; We will be working on making this process easier so that you do not need to specify package versions.*&lt;/p&gt;

&lt;p&gt;Next, you need to set all of your environment variables for Elementary so that they use the same as your existing dbt project. A typical setup could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      - name: elementary
        namespace: elementary
        pip_url: elementary-data[platform]==0.6.3 git+https://github.com/potloc/elementary-ext.git
        executable: elementary_invoker
        settings:
        - name: project_dir
          kind: string
          value: ${MELTANO_PROJECT_ROOT}/transform/
        - name: profiles_dir
          kind: string
          value: ${MELTANO_PROJECT_ROOT}/transform/profiles/platform/
        - name: file_path
          kind: string
          value: ${MELTANO_PROJECT_ROOT}/path/to/report.html
        - name: skip_pre_invoke
          env: ELEMENTARY_EXT_SKIP_PRE_INVOKE
          kind: boolean
          value: true
          description: Whether to skip pre-invoke hooks which automatically run dbt clean and deps
        - name: slack-token
          kind: password
        - name: slack-channel-name
          kind: string
          value: elementary-notifs
        config:
          profiles-dir: ${MELTANO_PROJECT_ROOT}/transform/profiles/platform/
          file-path: ${MELTANO_PROJECT_ROOT}/path/to/report.html
          slack-channel-name: your_channel_name
          skip_pre_invoke: true
        commands:

          initialize:
            args: initialize
            executable: elementary_extension
          describe:
            args: describe
            executable: elementary_extension
          monitor-report:
            args: monitor-report
            executable: elementary_extension
          monitor-send-report:
            args: monitor-send-report
            executable: elementary_extension

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

&lt;/div&gt;



&lt;p&gt;Make sure to &lt;strong&gt;specify the platform&lt;/strong&gt;, which should be specified in your profile (we use bigquery).&lt;/p&gt;

&lt;p&gt;After this, simply follow the instructions in the &lt;a href="https://docs.elementary-data.com/quickstart" rel="noopener noreferrer"&gt;Elementary Quickstart Guide&lt;/a&gt; to get the plugin up and running.&lt;/p&gt;

&lt;h1&gt;
  
  
  Generating your first report
&lt;/h1&gt;

&lt;p&gt;Once you have got elementary up and running, it's time to generate your first report. Simply run the command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meltano invoke elementary:monitor-report

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

&lt;/div&gt;



&lt;p&gt;and a brand new report should be generated at the specified &lt;code&gt;file-path&lt;/code&gt; (&lt;code&gt;${MELTANO_PROJECT_ROOT}/path/to/report.html&lt;/code&gt; in our case).&lt;/p&gt;

&lt;h1&gt;
  
  
  Next steps
&lt;/h1&gt;

&lt;p&gt;Once you've generated your first report, the sky is the limit in terms of integrating elementary to your workflow.&lt;/p&gt;

&lt;p&gt;The Elementary team has made it incredibly easy to send a report as a slack message. At Potloc, we receive reports twice a day to monitor the state of our pipeline.&lt;/p&gt;

&lt;p&gt;You can also set up hosting for your report on s3 or send slack alerts when an error occurs. Experiment and find what best works for you!&lt;/p&gt;

&lt;h1&gt;
  
  
  A quick closing statement
&lt;/h1&gt;

&lt;p&gt;While incredibly powerful, Elementary &lt;strong&gt;is not a replacement for best practices&lt;/strong&gt;.&lt;br&gt;
When writing tests, ensure that they are &lt;em&gt;pertinent&lt;/em&gt; and &lt;em&gt;targeted&lt;/em&gt;.&lt;br&gt;
Tests should be written to identify data integrity issues that can compromise business insights, not simply to identify null values.&lt;br&gt;
If you write too many tests without thinking of the meaning behind them, you run the risk of falling into the &lt;em&gt;"too many errors = no errors"&lt;/em&gt; paradigm where you have so many warnings that it's impossible to differentiate between actual issues and unfixable noise. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I speak from experience; at one point, we had multiple tests in our pipeline that returned a warning of over &lt;em&gt;5 000 erroneous values&lt;/em&gt;, with one going up to &lt;em&gt;180 000&lt;/em&gt;. These errors were unactionable, and yet the tests remained. Even with Elementary, this made it hard for us to differentiate between useless warnings and &lt;strong&gt;actual integrity issues&lt;/strong&gt; that needed to be resolved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Make sure to reach out to the Elementary team if you have any questions about their product!&lt;/p&gt;

&lt;p&gt;Interested in what we do at &lt;a href="https://www.potloc.com/" rel="noopener noreferrer"&gt;Potloc&lt;/a&gt;? Come join us! &lt;a href="https://jobs.lever.co/Potloc" rel="noopener noreferrer"&gt;We are hiring &lt;/a&gt; 🚀&lt;/p&gt;

</description>
      <category>emptystring</category>
    </item>
    <item>
      <title>How to optimize factory creation.</title>
      <dc:creator>Clément Morisset</dc:creator>
      <pubDate>Wed, 21 Dec 2022 20:24:45 +0000</pubDate>
      <link>https://dev.to/potloc/how-to-optimize-factory-creation-3bnp</link>
      <guid>https://dev.to/potloc/how-to-optimize-factory-creation-3bnp</guid>
      <description>&lt;p&gt;At &lt;strong&gt;&lt;a href="https://www.potloc.com/" rel="noopener noreferrer"&gt;Potloc&lt;/a&gt;&lt;/strong&gt; we have a test stack which is pretty standard in the Rails ecosystem. We run tests with &lt;strong&gt;RSpec&lt;/strong&gt;, we use &lt;strong&gt;FactoryBot&lt;/strong&gt; for setting up our test data, &lt;strong&gt;Capybara&lt;/strong&gt; for user interactions, &lt;strong&gt;Github Actions&lt;/strong&gt; as a CI etc..&lt;/p&gt;

&lt;p&gt;These great tools allow us to code at a fast pace with good test coverage. But this pace comes at a cost. The more the team grows, the bigger the codebase gets and the more tests get written.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤕 The issue
&lt;/h3&gt;

&lt;p&gt;As developer we usually take care of optimizing our own code and queries, we are used to test new implementations with all the edge cases. But tests optimization is likely a topic that we put &lt;strong&gt;at the bottom of the list&lt;/strong&gt; and that’s if we ever even think about it.&lt;br&gt;
Up until the moment where quietly but surely you will end up with a CI that take ages to run and a whole test suite to speed up.&lt;/p&gt;

&lt;p&gt;Let’s see how we took on this challenge at &lt;strong&gt;Potloc&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the purpose of demonstration we will rely on this simple test file:&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="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Purging&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QuestionnaireWorker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :worker&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;:questionnaire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:questionnaire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"#perform"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"destroys a survey form"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&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;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questionnaire&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="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Questionnaire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s2"&gt;"given associations"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"destroys a questionnaire and its associations"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:postal_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;questionnaire: &lt;/span&gt;&lt;span class="n"&gt;questionnaire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&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;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questionnaire&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="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SurveyQuestion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&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="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;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🧑‍🚒 The solutions
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/thoughtbot/factory_bot" rel="noopener noreferrer"&gt;factory-bot&lt;/a&gt; gem is used in almost in all of our spec files and it make our set up much more easier than when we use fixtures. &lt;br&gt;
Here is the tradeoff, the easier the gem is to use, the  more likely you’ll end up with some pain to control its usage. And when the times come to tackle slow tests, &lt;strong&gt;the best bet you can take is to start digging into you factories&lt;/strong&gt; because it’s likely they are the primary reason why your test suite is slowing down&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Avoiding the factory cascades&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To quote &lt;a href="https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest" rel="noopener noreferrer"&gt;Evil Martian&lt;/a&gt; a factory cascade is an&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;uncontrollable process of generating excess data through nested factory invocations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our test we use two factories a &lt;code&gt;questionnaire&lt;/code&gt; and &lt;code&gt;question&lt;/code&gt; who could be represented like this as a tree:&lt;br&gt;
&lt;/p&gt;

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

question
|
|---- survey
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this simple example each factory calls a nested factory. This means that every time we create a &lt;code&gt;questionnaire&lt;/code&gt; factory, we also create a &lt;code&gt;survey&lt;/code&gt; factory.&lt;/p&gt;

&lt;p&gt;To have a better vision of what objects are created in our spec file we can use &lt;a href="https://test-prof.evilmartians.io/" rel="noopener noreferrer"&gt;test-prof&lt;/a&gt;, a powerful gem that provides a collection of different tools to analyse your test suite performance. One of this tool is really useful to &lt;strong&gt;identify a factory cascade&lt;/strong&gt;, let’s introduce &lt;a href="https://test-prof.evilmartians.io/#/profilers/factory_prof" rel="noopener noreferrer"&gt;factory profiler&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If we run &lt;code&gt;FPROF=1 bundle exec rspec&lt;/code&gt; the factory profiler, it will generate the following report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TEST PROF INFO] Factories usage

 Total: 6
 Total top-level: 3
 Total time: 00:01.005 (out of 00:26.087)
 Total uniq factories: 3

   total   top-level     total time      time per call      top-level time               name

       3           0        0.6911s            0.2304s             0.0000s             survey
       2           2        0.6397s            0.3199s             0.6397s             questionnaire
       1           1        0.3658s            0.3658s             0.3658s             question
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most interesting insight is the &lt;strong&gt;difference&lt;/strong&gt; between the &lt;code&gt;total&lt;/code&gt; and the &lt;code&gt;top-level&lt;/code&gt;. The more this difference is important, the more you end up with factory cascade, meaning that &lt;strong&gt;you are creating useless factories&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let’s take the &lt;code&gt;survey&lt;/code&gt; , we don’t instantiate any surveys in our test suite but during the execution we create 4.&lt;/p&gt;

&lt;p&gt;The easiest workaround it is to instantiate a &lt;code&gt;survey&lt;/code&gt; at the top-level and to associate the factories to it.&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="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Purging&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QuestionnaireWorker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :worker&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;:survey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:survey&lt;/span&gt;&lt;span class="p"&gt;)&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;:questionnaire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:questionnaire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;survey: &lt;/span&gt;&lt;span class="n"&gt;survey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"destroys a questionnaire and its associations"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:postal_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;questionnaire: &lt;/span&gt;&lt;span class="n"&gt;questionnaire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;survey: &lt;/span&gt;&lt;span class="n"&gt;survey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we run the factory profiler we now have a different report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TEST PROF INFO] Factories usage

 Total: 5
 Total top-level: 5
 Total time: 00:00.868 (out of 01:09.067)
 Total uniq factories: 3

   total   top-level     total time      time per call      top-level time               name

       2           2        0.6986s            0.3493s             0.6986s             survey
       2           2        0.0973s            0.0487s             0.0973s             questionnaire
       1           1        0.0729s            0.0729s             0.0729s             question
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice! No more factory cascades. The &lt;code&gt;total&lt;/code&gt; and the &lt;code&gt;top-level&lt;/code&gt; columns are the same. &lt;strong&gt;We now create 5 factories instead of 8&lt;/strong&gt;. We have decreased the time spent creating factories &lt;strong&gt;by 30%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The caveat of this method it that it could be &lt;strong&gt;a heavy process to maintain&lt;/strong&gt;. Thankfully, &lt;code&gt;test-prof&lt;/code&gt; as a recipe called &lt;code&gt;FactoryDefault&lt;/code&gt;. Removing factory cascades manually could be good enough most of the time but if you want to go further you can follow the &lt;a href="https://test-prof.evilmartians.io/#/recipes/factory_default?id=instructions" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That being said, &lt;code&gt;test-prof&lt;/code&gt; has even more to offer, it’s time to introduce an awesome helper named &lt;code&gt;let_it_be&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reuse the factory you need&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's bring a little bit of magic and introduce a new way to set up a shared test data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;let_it_be&lt;/code&gt; is a &lt;a href="https://test-prof.evilmartians.io/#/recipes/let_it_be" rel="noopener noreferrer"&gt;helper&lt;/a&gt; that allows you to reuse the same factory for all your spec file. In our example we don’t need to create 2 &lt;code&gt;survey&lt;/code&gt; and 2 &lt;code&gt;questionnaire&lt;/code&gt; we could re-use the same ones for all our file.&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="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Purging&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QuestionnaireWorker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :worker&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let_it_be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:survey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:survey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let_it_be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:questionnaire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:questionnaire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;survey: &lt;/span&gt;&lt;span class="n"&gt;survey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&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;If we run the factory profiler we now have a different report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TEST PROF INFO] Factories usage

 Total: 3
 Total top-level: 3
 Total time: 00:00.272 (out of 00:24.264)
 Total uniq factories: 3

   total   top-level     total time      time per call      top-level time               name

       1           1        0.2024s            0.2024s             0.2024s             survey
       1           1        0.0323s            0.0323s             0.0323s             questionnaire
       1           1        0.0375s            0.0375s             0.0375s             question
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now we only create the factories we need, by reusing the same ones throughout our file.&lt;/p&gt;

&lt;p&gt;Be aware that &lt;code&gt;let_it_be&lt;/code&gt; come with a &lt;a href="https://test-prof.evilmartians.io/#/recipes/let_it_be?id=caveats" rel="noopener noreferrer"&gt;caveat&lt;/a&gt; section. I strongly encourage you to read the documentation and use this powerful helper in accordance with your needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀  Conclusion
&lt;/h3&gt;

&lt;p&gt;Let’s take a step back and relish our improvements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Initial&lt;/th&gt;
&lt;th&gt;Without cascades&lt;/th&gt;
&lt;th&gt;With let_it_be&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Factories creation time&lt;/td&gt;
&lt;td&gt;00:01.00&lt;/td&gt;
&lt;td&gt;00:00.868&lt;/td&gt;
&lt;td&gt;00:00.272&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Numbers look nice for this simple example. But what is the impact in real life at &lt;strong&gt;Potloc&lt;/strong&gt;? &lt;/p&gt;

&lt;p&gt;So far we just applied this recipe for a specific folder of our codebase. Below the result by profiling locally that folder:&lt;/p&gt;

&lt;p&gt;Before we spent &lt;strong&gt;3.50 min&lt;/strong&gt; in factories creation, now &lt;strong&gt;2 min. (~ -50%)&lt;/strong&gt;&lt;br&gt;
Before we created &lt;strong&gt;6824&lt;/strong&gt; factories, now &lt;strong&gt;4378. (~ -35%)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;test-prof&lt;/code&gt; is the swiss army knife we needed to speed up our test suite. It’s still a long journey but by embracing this topic we have already taken an important step!&lt;/p&gt;

&lt;p&gt;Want to go further? Watch this &lt;a href="https://www.youtube.com/watch?v=eDMZS_fkRtk&amp;amp;ab_channel=parisrb" rel="noopener noreferrer"&gt;99 problems of slow test&lt;/a&gt; talk by &lt;a href="https://github.com/palkan" rel="noopener noreferrer"&gt;Vladimir Dementyev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interested in what we do at &lt;a href="https://www.potloc.com/" rel="noopener noreferrer"&gt;Potloc&lt;/a&gt;? Come join us! &lt;a href="https://jobs.lever.co/Potloc" rel="noopener noreferrer"&gt;We are hiring&lt;/a&gt; 🚀&lt;/em&gt;&lt;/p&gt;

</description>
      <category>emptystring</category>
    </item>
    <item>
      <title>Automatic "Ready for Review" Github Action</title>
      <dc:creator>Jérôme Parent-Lévesque</dc:creator>
      <pubDate>Fri, 01 Apr 2022 18:40:47 +0000</pubDate>
      <link>https://dev.to/potloc/automatic-ready-for-review-github-action-5eb6</link>
      <guid>https://dev.to/potloc/automatic-ready-for-review-github-action-5eb6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;TLDR&lt;/em&gt;: We wanted a GitHub Action to automatically assign reviewers and mark a draft pull request as "Ready for review" after our test suite passes. The final code can be found in &lt;a href="https://gist.github.com/jeromepl/02e70f3ea4a4e8103da6f96f14eb213c" rel="noopener noreferrer"&gt;this gist here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At Potloc, our continuous integration process involves, among other things, a GitHub workflow running on each &lt;code&gt;push&lt;/code&gt; that tests the code against our full test suite. This check must pass for a pull request to be merged.&lt;/p&gt;

&lt;p&gt;Our test suite has gotten to a size where it is difficult to run on a personal computer in a reasonable amount of time, hence our developers usually rely on this GitHub workflow to run the full test suite.&lt;/p&gt;

&lt;p&gt;The process looks something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push code for a new feature&lt;/li&gt;
&lt;li&gt;Create a new Pull Request in "Draft" mode&lt;/li&gt;
&lt;li&gt;Wait for all the tests to pass&lt;/li&gt;
&lt;li&gt;Mark the Pull Request as "Ready for review" and assign reviewers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note that we consider it a good practice to wait until tests pass before assigning reviewers in order to prevent notifying them only to realize that some more changes are necessary.&lt;/p&gt;

&lt;p&gt;In practice, we have an in-house tool to help us automate most of these tasks through the &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;GitHub CLI&lt;/a&gt;, but for a long time we didn't have a way to automatically mark a pull request as "Ready for review" when the all tests passed, meaning we had to wait and periodically check the status of each of our PR.&lt;/p&gt;

&lt;p&gt;Inspired by Artur Dryomov's excellent post on &lt;a href="https://arturdryomov.dev/posts/auto-github-pull-requests/" rel="noopener noreferrer"&gt;Autonomous GitHub Pull Requests&lt;/a&gt;, we set out to create a GitHub Action to help us automate this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution:
&lt;/h2&gt;

&lt;p&gt;At the moment of creating the draft pull request, we want to be able to specify what to do in the event that all tests pass.&lt;/p&gt;

&lt;p&gt;To achieve this, we will use a tag named &lt;code&gt;autoready&lt;/code&gt; that we can put on our pull requests to signify that this PR should be automatically marked as "Ready for review" when all tests pass.&lt;/p&gt;

&lt;p&gt;In addition, we want to be able to automatically assign reviewers when that happens. For that, we will be using a specific comment format that looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;autoready-reviewers: reviewer1,reviewer2,organization/team1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our workflow will automatically detect comments like this and assign each of the listed individual or team reviewers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Workflow configuration
&lt;/h3&gt;

&lt;p&gt;Our workflow should run after each run of our &lt;code&gt;Test&lt;/code&gt; workflow and use its output status to determine whether or not to mark the pull request as "Ready for review".&lt;br&gt;
&lt;code&gt;.github/workflows/ready_for_review.yml&lt;/code&gt;:&lt;/p&gt;

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

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ready For Review&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;branches-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;completed&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mark_as_ready_for_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.workflow_run.conclusion == 'success' }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mark as Ready for Review&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash .github/workflows/mark_as_ready_for_review.sh "${{ secrets.ACCESS_TOKEN }}" "${{ join(github.event.workflow_run.pull_requests.*.number) }}"&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This will run our custom script &lt;code&gt;mark_as_ready_for_review.sh&lt;/code&gt; after each &lt;strong&gt;successful&lt;/strong&gt; run of the &lt;code&gt;Test&lt;/code&gt; workflow.&lt;/p&gt;

&lt;p&gt;Some noteworthy points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need the &lt;code&gt;Checkout Code&lt;/code&gt; action to get the latest version of this &lt;code&gt;mark_as_ready_for_review.sh&lt;/code&gt; script.&lt;/li&gt;
&lt;li&gt;Our script takes a couple of arguments as input:

&lt;ol&gt;
&lt;li&gt;A &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" rel="noopener noreferrer"&gt;GitHub access token&lt;/a&gt; of the "user" on behalf of whom we will be performing these automatic actions. In our case, we have a dedicated bot account for this. We store this value in a &lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer"&gt;GitHub secret&lt;/a&gt; &lt;code&gt;secrets.ACCESS_TOKEN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A comma-separated list of all pull request IDs associated with this workflow run. Since a workflow run is attached to a particular commit hash, it is possible that multiple PRs have that same commit hash as &lt;code&gt;HEAD&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bash Script
&lt;/h3&gt;

&lt;p&gt;Here is the script dissected and explained (scroll to the bottom for the full script):&lt;/p&gt;

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

&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eou&lt;/span&gt; pipefail &lt;span class="c"&gt;# Make sure we get useful error messages on failure&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Our inputs and constants:&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;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;PR_NUMBERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"autoready"&lt;/span&gt; &lt;span class="c"&gt;# the name of the 'label' on the PR used to detect whether or not this script should run&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-repository"&lt;/span&gt; &lt;span class="c"&gt;# the name of your repository on GitHub&lt;/span&gt;
&lt;span class="nv"&gt;ORGANIZATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"potloc"&lt;/span&gt; &lt;span class="c"&gt;# the name of your GitHub organization or user to which the repository belongs&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Then, we want to repeat the whole thing for as many pull requests as have been passed as input:&lt;/p&gt;

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

&lt;span class="c"&gt;# Split the numbers string (comma-delimited)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;pr_number &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$PR_NUMBERS&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s2"&gt;","&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Fetch the labels from the pull request. We will also need the Node ID of the PR to use GitHub's GraphQL API in a later step, so we also grab this at the same time.&lt;/p&gt;

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

&lt;span class="c"&gt;# Get the node_id (and labels) from the PR number&lt;/span&gt;
&lt;span class="c"&gt;# - https://docs.github.com/en/graphql/guides/using-global-node-ids&lt;/span&gt;
&lt;span class="c"&gt;# - https://docs.github.com/en/rest/reference/pulls#get-a-pull-request&lt;/span&gt;
&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github.v3+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: token &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--request&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.github.com/repos/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORGANIZATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pulls/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pr_number&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;node_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.node_id'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;contains_label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="s2"&gt;"any(.labels[].name == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; .)"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;comments_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".comments_url"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Check if the PR contains the label we want&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$contains_label&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# Continued below&lt;/span&gt;


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

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that we use &lt;a href="https://stedolan.github.io/jq/" rel="noopener noreferrer"&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/a&gt; to simplify parsing of the JSON body returned by the GitHub API. This needs to be installed on the workers that will run this Workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the label exists on the PR, then we can mark is as "Ready for review". This API only exists in GitHub's &lt;a href="https://docs.github.com/en/graphql" rel="noopener noreferrer"&gt;GraphQL API&lt;/a&gt;, hence the different request. This is where we make use of the previously-retrieved &lt;code&gt;node_id&lt;/code&gt;:&lt;/p&gt;

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

&lt;span class="c"&gt;# Mark the PR as ready for review&lt;/span&gt;
curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: token &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"{ &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;query&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;mutation { markPullRequestReadyForReview(input: { pullRequestId: &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;node_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; }) { pullRequest { id } } }&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; }"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://api.github.com/graphql


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

&lt;/div&gt;

&lt;p&gt;Delete the label to prevent running this script for this PR:&lt;/p&gt;

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

&lt;span class="c"&gt;# Remove the label&lt;/span&gt;
curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; &lt;span class="s2"&gt;"DELETE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github.v3+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: token &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.github.com/repos/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORGANIZATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/issues/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pr_number&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/labels/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LABEL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Finally, we want to find which reviewers to assign to this PR. To do this, we fetch all comments on the PR and use a regex to find a comment matching our &lt;code&gt;autoready-reviewers:&lt;/code&gt; format we defined:&lt;/p&gt;

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

&lt;span class="c"&gt;# Get the comments on the PR&lt;/span&gt;
&lt;span class="nv"&gt;comments_out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/vnd.github.v3+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: token &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--request&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="nv"&gt;$comments_url&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Look for a comment matching the 'autoready-reviewers: ' pattern&lt;/span&gt;
&lt;span class="c"&gt;# If found, assign the mentionned reviewers to review this PR&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".[].body"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$comments_out&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nb"&gt;read &lt;/span&gt;comment&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ autoready-reviewers:[[:space:]]&lt;span class="o"&gt;([&lt;/span&gt;a-zA-Z0-9,&lt;span class="se"&gt;\-\/&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;all_reviewers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_REMATCH&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# Get the first matching group of the regex (the comma-separated list of reviewers)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Using this list of reviewers, we differentiate between teams (e.g. &lt;code&gt;potloc/devs&lt;/code&gt;) and individuals to assign by looking for the &lt;code&gt;/&lt;/code&gt; character:&lt;/p&gt;

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

&lt;span class="c"&gt;# Split the reviewers between teams and individuals&lt;/span&gt;
&lt;span class="nv"&gt;reviewers_array&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="nv"&gt;team_reviewers_array&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;reviewer &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$all_reviewers&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s2"&gt;","&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&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
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$reviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="o"&gt;[&lt;/span&gt;a-zA-Z0-9,&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;a-zA-Z0-9,&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;+ &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c"&gt;# In the case of a team reviewer, only take the part of the username after the '/':&lt;/span&gt;
    &lt;span class="nv"&gt;slug_array&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;//\// &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;team_slug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;slug_array&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    team_reviewers_array+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$team_slug&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;reviewers_array+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$reviewer&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# Join the array elements into a single comma-separated string:&lt;/span&gt;
&lt;span class="nv"&gt;reviewers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;, &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;reviewers_array&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;team_reviewers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;, &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;team_reviewers_array&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The very last step is to make the API call to assign these individual and teams as reviewers to the PR:&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="c"&gt;# Assign reviewers&lt;/span&gt;&lt;br&gt;
curl &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; /dev/null &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github.v3+json"&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: token &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;a href="https://api.github.com/repos/" rel="noopener noreferrer"&gt;https://api.github.com/repos/&lt;/a&gt;&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ORGANIZATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pulls/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pr_number&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/requested_reviewers"&lt;/span&gt; &lt;span class="se"&gt;&amp;lt;/span&amp;gt;&lt;br&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;reviewers&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;reviewers&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;], &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;team_reviewers&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;team_reviewers&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]}"&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&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;And that is it! Now, to use this tool we can put the &lt;code&gt;autoready&lt;/code&gt; label on a draft pull request and write a comment in the form &lt;code&gt;autoready-reviewers: reviewer1,reviewer2,organization/team1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In practice, at Potloc, we have a little helper in-house tool do these steps for us using the &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;GitHub CLI&lt;/a&gt; and &lt;a href="https://github.com/piotrmurach/tty-prompt" rel="noopener noreferrer"&gt;&lt;code&gt;tty-prompt&lt;/code&gt;&lt;/a&gt; to&lt;br&gt;
ease the selection of reviewers/teams and the formatting of this comment.&lt;/p&gt;

&lt;p&gt;And this is what it looks like on GitHub's interface!&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36rnqut08bzusrjv7tzw.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36rnqut08bzusrjv7tzw.png" alt="GitHub interface flow"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interested in what we do at Potloc? Come join us! &lt;a href="https://jobs.lever.co/Potloc?team=Product%20and%20Dev" rel="noopener noreferrer"&gt;We are hiring&lt;/a&gt;&lt;/em&gt; 🚀&lt;/p&gt;

&lt;p&gt;Full code:&lt;br&gt;
&lt;a href="https://gist.github.com/jeromepl/02e70f3ea4a4e8103da6f96f14eb213c" rel="noopener noreferrer"&gt;https://gist.github.com/jeromepl/02e70f3ea4a4e8103da6f96f14eb213c&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>Streamline a service desk with JIRA automation</title>
      <dc:creator>Clément Morisset</dc:creator>
      <pubDate>Mon, 17 Jan 2022 12:22:38 +0000</pubDate>
      <link>https://dev.to/potloc/streamline-a-service-desk-with-jira-automation-2kfg</link>
      <guid>https://dev.to/potloc/streamline-a-service-desk-with-jira-automation-2kfg</guid>
      <description>&lt;p&gt;At &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt; our service desk is dedicated for the operation teams where they can open a ticket for a bug or a service request. The more our web application grows, the more maintenance it requires. As a result we’ve seen a growing number of tickets pertaining to the same project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The need
&lt;/h2&gt;

&lt;p&gt;We wanted a way to link these tickets together to gather additional context.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Were there any issues on this project in the past?&lt;/li&gt;
&lt;li&gt;What was the solution?&lt;/li&gt;
&lt;li&gt;Which developer worked on it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions can be crucial in getting the visibility required when it comes to debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;One word: &lt;code&gt;Automation&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This aptly named feature  allows you to automate  tasks through a  series of actions triggered by specific events.&lt;/p&gt;

&lt;p&gt;Let’s link all created tickets for the same project using automation.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The how
&lt;/h2&gt;

&lt;p&gt;In our particular case the event is &lt;code&gt;When a new issue is created&lt;/code&gt; . The entry point for grouping tickets by a specific project is the url field. Each request has a link to easily locate the issue. In 99% of case our url will have the ID of a project. So the first action that you need to process after the creation of an issue is to scan the URL  in order to extract the project ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Extract the survey ID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can have a hidden field that is not displayed to the user but will allow you to store the extracted ID.&lt;br&gt;
Let's add a &lt;code&gt;New component&lt;/code&gt; &amp;gt; &lt;code&gt;New action&lt;/code&gt; &amp;gt; &lt;code&gt;Edit an issue&lt;/code&gt; and the field you want to use. For the purpose of this article we will use &lt;code&gt;Root cause&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Above we are using &lt;code&gt;Smart values&lt;/code&gt;, according to Jira &lt;a href="https://support.atlassian.com/cloud-automation/docs/jira-smart-values-issues/"&gt;documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Smart values allow you to access issue data within Jira. For example, you can use the following smart values to send a Slack message that includes the issue key and issue summary: &lt;code&gt;{{issue.key}} {{issue.summary}}&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;customfield_10364&lt;/code&gt;: is the ID for our URL field that contains the project ID. You can go to settings or inspect your input to find it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;substringBetween&lt;/code&gt;: is a &lt;a href="https://support.atlassian.com/cloud-automation/docs/jira-smart-values-text-fields/"&gt;built-in function&lt;/a&gt; that returns the text between the given parameters.&lt;/p&gt;

&lt;p&gt;From this moment your field will contain the ID. But to have an up to date flow (with the extracted ID) we have to refetch the data issue by adding a new action &lt;code&gt;Re-fetch data issue.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, we would like to link issues with the same project ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Find linked issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Jira has an action for this called Lookup issues.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;cf[10359]&lt;/code&gt; : is the identifier of our &lt;code&gt;Root cause&lt;/code&gt; field (replace the number with your ID)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;issue.customfield_10359&lt;/code&gt; : is the identifier of the &lt;code&gt;Root cause&lt;/code&gt; field for the other issues&lt;/p&gt;

&lt;p&gt;&lt;code&gt;issueKey != "issue.key"&lt;/code&gt; : we exclude our current issue of the lookup&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Careful! Your fields will be named differently according to your need.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What we do, in pseudo code, is basically searching via &lt;code&gt;JQL&lt;/code&gt; if the extracted ID of the issue matches any previously-created issues. We also ensure we exclude the current issue of the search.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: In our context, Root cause is a text field. It's likely that you have to find another operator if the type of your field is different.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Link similar issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, we need to link all results from our previous query with our current issue using the &lt;code&gt;Link issue to action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it. From now on you should see previously-created tickets for this project on all your tickets.&lt;/p&gt;

&lt;p&gt;Here’s the final result:&lt;/p&gt;

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

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

&lt;p&gt;&lt;em&gt;Pro tips: You could add a condition as a guard clause to stop the automation if the Lookup issues actions does not return anything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Happy automation. 🎉&lt;/p&gt;

&lt;p&gt;Interested in what we do at &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt;? Come join us! We are &lt;a href="https://jobs.lever.co/Potloc"&gt;hiring&lt;/a&gt; 🚀&lt;/p&gt;

</description>
      <category>jira</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Optimally Taking Out Extra Survey Respondents</title>
      <dc:creator>Jérôme Parent-Lévesque</dc:creator>
      <pubDate>Tue, 05 Oct 2021 17:54:34 +0000</pubDate>
      <link>https://dev.to/potloc/optimally-taking-out-extra-survey-respondents-c30</link>
      <guid>https://dev.to/potloc/optimally-taking-out-extra-survey-respondents-c30</guid>
      <description>&lt;p&gt;Sometimes when analysing the results of a survey, one needs to remove some respondents from their sample. This is something we do fairly commonly at &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt; in order to obtain a more representative sample of the target population in our surveys. In other words, we use this as a way of performing &lt;em&gt;stratified sampling&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We use a system of &lt;em&gt;quotas&lt;/em&gt; to keep track of every agreement on the respondent sample we make with our clients. We have three types of quotas, each corresponding to a different way of assessing whether their target is met or not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To match targets &lt;em&gt;exactly&lt;/em&gt; (like we want to achieve for stratified sampling) we use one of these quota types - &lt;strong&gt;the &lt;code&gt;strict&lt;/code&gt; quota type&lt;/strong&gt;. For example, our clients might want &lt;em&gt;exactly&lt;/em&gt; 50 respondents who work as electricians. No matter whether we have one respondent missing or one more than 50 in this category, our quota is not achieved.&lt;/p&gt;

&lt;p&gt;The second type of quota we use is &lt;strong&gt;the &lt;code&gt;minimum&lt;/code&gt; type&lt;/strong&gt;. This type, as the name suggests, simply indicates that we must have at least as many respondents of a specific category as the target number.&lt;/p&gt;

&lt;p&gt;The third and final type of quota is &lt;strong&gt;the &lt;code&gt;weighted&lt;/code&gt; type&lt;/strong&gt;. As we often use a weighting process to obtain a more representative sample of our population, we make sure to communicate with our clients where survey responses may be weighted. This communication in turn gets converted into quotas of type &lt;code&gt;weighted&lt;/code&gt; which behave similarly to &lt;code&gt;minimum&lt;/code&gt; quotas, but with more flexibility. The targets don't need to be matched exactly and will instead be achieved through an independent weighting process (don't worry, this will be explained in more details in Step 2 below). The "minimum" for this type of quota is (arbitrarily) set to 50% of the target as a way to limit the scale of the weights (this way weights should rarely be more than 2).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ux2a8ojg41hpavjpxah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ux2a8ojg41hpavjpxah.png" alt="quotas" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image above shows an example of a combination of quotas we could have. Here, we want to end up with a minimum of one respondent who has a cat, exactly one respondent who is a doctor, and we want &lt;strong&gt;after weighting&lt;/strong&gt; to have one &lt;em&gt;effective&lt;/em&gt; respondent whose name contain the letter 'a' and two &lt;em&gt;effective&lt;/em&gt; respondents whose name is shorter than 7 letters.&lt;/p&gt;

&lt;p&gt;Imagine now that we have received the following responses to our survey:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvksz4qd8lg4mtn2bjwlr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvksz4qd8lg4mtn2bjwlr.png" alt="respondents" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this initial state, we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 respondent who has a cat (Alice)&lt;/li&gt;
&lt;li&gt;2 doctors (Bob and Catherine)&lt;/li&gt;
&lt;li&gt;2 respondents whose name contains the letter 'a' (Alice and Catherine)&lt;/li&gt;
&lt;li&gt;2 respondents whose name is shorter than 7 letters (Alice and Bob)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The minimum quota is therefore satisfied (but Alice cannot be removed without breaking it) and there is one too many doctor. For weighted quotas, we always have at least 50% of the target number of respondents. As we will see later, the weighted quotas will be useful in determining the optimal respondents to take out.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We will keep referring to these quotas and respondents throughout this article to provide a practical example of how we select respondents to take out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We set out to find the &lt;em&gt;optimal&lt;/em&gt; selection of respondents to take out given a set of quotas such as this one. Below is the full step-by-step explanation of the algorithm we use to perform this and an example of how it is applied to this fictional set of quotas and respondents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1
&lt;/h2&gt;

&lt;p&gt;We first identified that by determining which respondents belonged to which quotas, we could split the respondents into 3 different categories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respondents that cannot be taken out&lt;/strong&gt; are respondents that belong to quotas for which the target is not exceeded. For example, our &lt;code&gt;minimum&lt;/code&gt; quota "has a cat" has a target of 1 and only Alice fits into this category. Therefore, Alice cannot be taken out as otherwise the "has a cat" quota would be broken. The same goes for quotas with fewer respondents than the target, for example if the target was to have 2 respondents who own a cat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respondents that should be taken out as a priority&lt;/strong&gt; are respondents that belong specifically to a &lt;code&gt;strict&lt;/code&gt; quota for which the target is exceeded. Since for this type of quota we want to end up with exactly the target number of respondents, we have to take out respondents belonging to this quota until that target is matched. This group takes priority over the &lt;em&gt;Respondents that cannot be taken out&lt;/em&gt; as we prioritise taking out respondents in exceeded strict quotas until those quotas are satisfied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respondents that may be taken out&lt;/strong&gt; are all remaining respondents. These respondents may or may not belong to any quota. If they do, then that quota's target has to be exceeded — otherwise they would be in the &lt;em&gt;Respondents that cannot be taken out&lt;/em&gt; category. Note that these respondents logically cannot belong to any &lt;code&gt;strict&lt;/code&gt; quota since those belonging to this type of quota must fit in one of the first 2 categories.&lt;/p&gt;

&lt;p&gt;Going back to our example, our three respondents would belong into the following groups:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdt49f1dpj3ka5vaga5uh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdt49f1dpj3ka5vaga5uh.png" alt="buckets" width="800" height="325"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The &lt;em&gt;respondents that should be taken out as a priority&lt;/em&gt; group includes both doctors (Bob and Catherine) as there is one too many to satisfy the strict quota. Alice cannot be taken out because she is the only respondent who has a cat. The last group is empty as all respondents already belong to other groups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2
&lt;/h2&gt;

&lt;p&gt;Now that we have a categorisation of each respondent, we are almost ready to start taking out respondents. However, since our objective is to &lt;em&gt;optimally&lt;/em&gt; take out respondents, we need to compute one more piece of data related to &lt;code&gt;weighted&lt;/code&gt;-type quotas.&lt;/p&gt;

&lt;p&gt;First, we need to define a bit better what we mean by &lt;em&gt;optimally&lt;/em&gt; here.&lt;br&gt;
Our survey results are usually calculated on &lt;em&gt;weighted&lt;/em&gt; data in order to better match the target population demographics. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In other words, as part of our survey workflow, we compute a weight for each respondent and use it as a multiplicative factor to scale the "importance" of each survey response. This is a process called &lt;em&gt;weighting&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The weights can be interpreted as a measure of the quality of our respondents sample by looking at their distribution. The further away from the value of 1 the weights are, the worse the quality. Indeed, a small weight indicates that we have too many similar respondents and a large weight indicates that we are missing respondents with similar characteristics.&lt;br&gt;
For more details on the weighting process I invite you to read &lt;a href="https://dev.to/potloc/generalized-raking-for-survey-weighting-2d1d"&gt;my previous blog post on &lt;em&gt;Generalized Weighting&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thus, when taking out extra respondents, we would like to ensure that our weighting quality will be unaffected. &lt;strong&gt;This is the key to our notion of &lt;em&gt;optimality&lt;/em&gt; — we want not only to satisfy all quotas but also to obtain the highest possible weighting quality as a result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To achieve this, we compute weights for each respondent based on the targets of the &lt;code&gt;weighted&lt;/code&gt; quotas. Using a &lt;a href="https://dev.to/potloc/generalized-raking-for-survey-weighting-2d1d"&gt;raked weighting algorithm&lt;/a&gt;, we use all weighted quota numbers as "targets" to obtain respondent weights.&lt;/p&gt;

&lt;p&gt;In our example, we obtain the following weights by using the targets from the two weighted quotas:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr3r0k2xyw44ea3e1wbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr3r0k2xyw44ea3e1wbm.png" alt="weights" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that by multiplying the respondent's (numerical) answer the weighted quota targets are matched perfectly! The count of respondents whose name contains the letter 'a' becomes 1 (from 2) as it is now the sum of 0.59 and 0.41. Meanwhile, the count of respondents whose name is shorter than 7 letters stays 2, although the weight of each respondent differs.&lt;/p&gt;

&lt;p&gt;In the next step, we will be removing respondents with the smallest weights first whenever we cannot decide who to take out!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3
&lt;/h2&gt;

&lt;p&gt;Our respondents now belong to one of the 3 categories presented in Step 1 and each have a weight resulting from the raked weighting computation from Step 2. It is now time to start taking out respondents.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The core of the strategy here is to take out respondents &lt;strong&gt;one-by-one&lt;/strong&gt;. After each respondent that is taken out, our quotas and weighting need to be updated, meaning that steps 1 and 2 need to be performed again! We therefore perform this step in a loop where in each iteration we recompute the first 2 steps before choosing and taking out 1 respondent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This respondent is chosen according to the given priority list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select a pool of respondents to pick from:

&lt;ul&gt;
&lt;li&gt;If there are any respondents in the &lt;em&gt;Respondents that should be taken out as a priority&lt;/em&gt; category, then limit our selection to this group only&lt;/li&gt;
&lt;li&gt;Otherwise, if there are any &lt;em&gt;Respondents that may be taken out&lt;/em&gt;, select this group&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;From this pool, select the optimal respondent to be taken out:

&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;optimal&lt;/em&gt; respondent corresponds to the respondent with the &lt;strong&gt;smallest&lt;/strong&gt; weight, since a small weight indicates that we have many similar respondents&lt;/li&gt;
&lt;li&gt;In the case of a tie, or if there are no &lt;code&gt;weighted&lt;/code&gt; quotas, we remove the last respondent to have answered the survey. (&lt;em&gt;Note: the statistically correct thing to do here would be to remove a random respondent from the pool, but we choose this approach as it is idempotent — we can re-run the algorithm and the selected respondents will be the same. Additionally, this replicates the behaviour of traditional sampling tools that have quota-stops&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Take out the selected respondent and repeat from Step 1 until all &lt;code&gt;strict&lt;/code&gt; quotas are met!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how this would play out in our fictional example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have 2 respondents in the &lt;em&gt;Respondents that should be taken out as a priority&lt;/em&gt; category (Bob and Catherine) and thus only these respondents are taken into consideration&lt;/li&gt;
&lt;li&gt;To determine who to remove from these two respondents, we take a look at their data:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5saox2kwrjoxu683v5hp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5saox2kwrjoxu683v5hp.png" alt="decision-respondents" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Since Catherine has the smallest weight (0.41 vs. 1.41), she is taken out. Intuitively, this makes sense as we had one too many respondent whose name contained the letter 'a' to satisfy the weighted quota without even applying a weighting. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are now left with one respondent whose name contains the letter 'a' and two respondents whose name is shorter than 7 letters, meaning that our final weights will be exactly 1 — the optimal value for weights!&lt;/p&gt;

&lt;p&gt;Additionally, now that respondent "Catherine" has been taken out, all of our quotas are satisfied and we can stop the algorithm here.&lt;/p&gt;

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

&lt;p&gt;Using this process, we are able to remove respondents so as to match our quotas as best as we can, while also leading to a better survey weighting. Indeed, since we always remove respondents with the smallest weights, our weighting gets progressively better as the minimum weight gets closer and closer to 1 (the optimal value). This means that the final data presented for this survey — after the weighting step — will be more representative of the target population, a win for both Potloc and our clients!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interested in what we do at Potloc? Come join us! &lt;a href="https://jobs.lever.co/Potloc?team=Product%20and%20Dev"&gt;We are hiring&lt;/a&gt; 🚀&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Appendix - The case of multiple overlapping &lt;code&gt;strict&lt;/code&gt; quotas
&lt;/h3&gt;

&lt;p&gt;We might sometimes have respondents that correspond to multiple different &lt;code&gt;strict&lt;/code&gt; quotas. In this scenario, it is more complicated to select the optimal respondents to take out as it is not always obvious what is the smallest possible set of respondents that need to be removed in order to satisfy all such quotas. It is, for example, possible to have a respondent (let's call them respondent A) that we take out since it belongs to a &lt;code&gt;strict&lt;/code&gt; quota which have both exceeded their target. However, it is possible to then take out other respondents which also correspond to this quota because they also belong to another &lt;code&gt;strict&lt;/code&gt; quota which was exceeded. This could now break the first quota if it had met its target exactly. In this scenario, we end up with a respondent (respondent A) which can now be reinstated as the strict quota it belonged to is now under its target.&lt;/p&gt;

&lt;p&gt;To alleviate this problem while avoiding a complicated and expensive decision process solutions from the field of operational research, we employ two mechanisms.&lt;/p&gt;

&lt;p&gt;First, we try to more optimally pick which &lt;code&gt;strict&lt;/code&gt; quota respondents to take out first. To do this, we also consider the following factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the respondent can be disqualified or not (if all of its quotas are exceeded)&lt;/li&gt;
&lt;li&gt;The number of exceeded &lt;code&gt;strict&lt;/code&gt; quotas a respondent is a part of (more = higher priority)&lt;/li&gt;
&lt;li&gt;The total number of &lt;code&gt;strict&lt;/code&gt; quotas a respondent is a part of (fewer = higher priority)

&lt;ul&gt;
&lt;li&gt;This is used to minimise the impact of taking out respondents on other quotas&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;The minimum difference between the current count and the target count of &lt;code&gt;strict&lt;/code&gt; quotas that are exceeding their target (bigger = higher priority, as there is more room to remove respondents)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Second, we add a final step at the end of the process in which we &lt;em&gt;restore&lt;/em&gt; respondents that can be without breaking any quota. This solves the issue highlighted in the example above.&lt;/p&gt;

&lt;p&gt;Using these two approximations we are able to get a result that is close to optimal, for a minimal cost.&lt;/p&gt;

</description>
      <category>survey</category>
      <category>sampling</category>
      <category>statistics</category>
    </item>
    <item>
      <title>How to use Sentry for profiling a test suite</title>
      <dc:creator>Clément Morisset</dc:creator>
      <pubDate>Mon, 20 Sep 2021 20:11:37 +0000</pubDate>
      <link>https://dev.to/potloc/how-to-use-sentry-for-profiling-a-test-suite-30l7</link>
      <guid>https://dev.to/potloc/how-to-use-sentry-for-profiling-a-test-suite-30l7</guid>
      <description>&lt;p&gt;At &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt; one of our core values is learning, that's why each quarter the development team has a dedicated time to explore new things. This aptly named &lt;strong&gt;Dev Happiness Week&lt;/strong&gt; allows us to tackle either a new pattern, open source project, write a blog post and so on.&lt;/p&gt;

&lt;p&gt;One of my initial projects was to monitor our test suite. The more our test suite was growing quickly the more seconds were added to our &lt;code&gt;CI&lt;/code&gt;. But without any monitoring on how long each test takes to run it was complicated to identify and speed up the slowest ones. Let's fix this by asking the following question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What is the minimal valuable move to have a test profiling dashboard ?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We use &lt;strong&gt;Sentry&lt;/strong&gt; for error tracking and they have a great feature for &lt;a href="https://sentry.io/for/performance/"&gt;performance&lt;/a&gt; monitoring that allow you to track queries and get transactions duration.&lt;/p&gt;

&lt;p&gt;Hence the following steps will show you how to twist the performance overview page of Sentry into a test suite monitoring. 🤓&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimers: The following example assume that you use &lt;code&gt;RSpec&lt;/code&gt;  gem and that you already have a dedicated Sentry environment for it (eg: &lt;code&gt;app-test-suite&lt;/code&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We would like to satisfy three specifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cover all of our test suite&lt;/li&gt;
&lt;li&gt;Don't flood our monitoring with unnecessary data (eg: unit tests that run in under 1 second)&lt;/li&gt;
&lt;li&gt;Make it easy to locate our slower tests&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cover all of our test suite
&lt;/h3&gt;

&lt;p&gt;The easy one. Let's start by creating a &lt;code&gt;profiling.rb&lt;/code&gt; file that will be run in our test suite, add an &lt;a href="https://relishapp.com/rspec/rspec-core/v/2-0/docs/hooks/around-hooks"&gt;around hook&lt;/a&gt; and compute the elapsed time between the beginning of a run and the end.&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="c1"&gt;#spec/support/config/profiling.rb&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&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;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;)&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;example&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;example_start&lt;/span&gt; &lt;span class="o"&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="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

    &lt;span class="n"&gt;example_end&lt;/span&gt; &lt;span class="o"&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="n"&gt;elapsed_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;example_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;example_start&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;Nice, we now cover the first bullet point of our specification. &lt;/p&gt;

&lt;h3&gt;
  
  
  Don't flood our monitoring with unnecessary data
&lt;/h3&gt;

&lt;p&gt;The purpose here is to avoiding flooding Sentry when everything work as expected. We just want to send data when a test is slow or takes longer than expected.&lt;/p&gt;

&lt;p&gt;We don't have the same expectations between a system test and a unit test. Hence we have to set a different thresholds accordingly:&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="c1"&gt;#spec/support/config/profiling.rb&lt;/span&gt;

&lt;span class="no"&gt;SLOW_EXECUTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="no"&gt;MEDIUM_EXECUTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"integration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="no"&gt;SLOW_EXECUTION_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="no"&gt;MEDIUM_EXECUTION_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="no"&gt;DEFAULT_EXECUTION_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&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;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;)&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;example&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;example_start&lt;/span&gt; &lt;span class="o"&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="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

    &lt;span class="n"&gt;example_end&lt;/span&gt; &lt;span class="o"&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="n"&gt;elapsed_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;example_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;example_start&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;execution_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;"Slow alert!"&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="c1"&gt;# input: "./spec/integration/restaurants/owner_spec.rb:16"&lt;/span&gt;
&lt;span class="c1"&gt;# output: "integration"&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;spec_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:location&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./spec/"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;%r{^[^/]*}&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="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&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;category&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="no"&gt;SLOW_EXECUTION&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="no"&gt;SLOW_EXECUTION_THRESHOLD&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="no"&gt;MEDIUM_EXECUTION&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="no"&gt;MEDIUM_EXECUTION_THRESHOLD&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;DEFAULT_EXECUTION_THRESHOLD&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;Firstly we set our &lt;strong&gt;threshold constants&lt;/strong&gt;, corresponding maximum execution times. &lt;br&gt;
Then we add a &lt;code&gt;spec_category&lt;/code&gt; method that allows us to identify the type of test, and we check if the runtime is lower or higher than expected.&lt;br&gt;
If it does we print a beautiful message.&lt;/p&gt;

&lt;p&gt;We are almost there! We just have to generate a custom Sentry &lt;strong&gt;transaction&lt;/strong&gt; in order to populate the performance table with our profiling data.&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="c1"&gt;#spec/support/config/profiling.rb&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&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;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;)&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;example&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;example_start&lt;/span&gt; &lt;span class="o"&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="n"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sentry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Transaction&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="ss"&gt;op: &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;hub: &lt;/span&gt;&lt;span class="no"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_hub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;execution_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       
      &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;Sentry::Transaction&lt;/code&gt; takes 2 arguments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;op&lt;/strong&gt; : used for the name of the &lt;a href="https://github.com/getsentry/sentry-ruby/blob/3ddb146ed2feaf2d2c078c979629ab567a2701d8/sentry-ruby/spec/sentry/transaction_spec.rb#L10"&gt;operation&lt;/a&gt; (eg: &lt;code&gt;sql.query&lt;/code&gt; ). In our case we will populate it with the category of our spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hub&lt;/strong&gt;: Sentry requires an hub, we use the current. According to &lt;a href="https://docs.sentry.io/platforms/ruby/enriching-events/scopes/"&gt;documentation&lt;/a&gt; &lt;em&gt;"You can think of the hub as the central point that our SDKs use to route an event to Sentry"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By finishing our transaction we send our event to Sentry. From now on you should see your first tests appear on the performance dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G-XPsw-c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmmqxkt0k5u84e1ywz24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G-XPsw-c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmmqxkt0k5u84e1ywz24.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to read Sentry &lt;a href="https://docs.sentry.io/product/performance/metrics/"&gt;documentation&lt;/a&gt; for a better understanding the performance table.&lt;/p&gt;

&lt;p&gt;⚠️ By tweaking the performance dashboard of Sentry for our test suite we have to deal with the expected behaviour of Sentry. Such as send event when there is fail in your environnement&lt;/p&gt;

&lt;p&gt;That's why it's necessary to add the following configuration in our Sentry test initializer.&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="c1"&gt;#spec/support/config/sentry.rb&lt;/span&gt;

&lt;span class="no"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&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;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://4**************************************3"&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;excluded_exceptions&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Exception"&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 our context of profiling we won't send an event when a test fails on our &lt;code&gt;CI&lt;/code&gt;. For avoiding noise and flooding you have to exclude all &lt;strong&gt;exceptions&lt;/strong&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Make it easy to locate our slower tests
&lt;/h3&gt;

&lt;p&gt;By default the transaction name corresponds to the name of the controller where the transaction has been run. In the screenshot above it shows that our test uses the method &lt;code&gt;template&lt;/code&gt; in &lt;code&gt;ChartsController&lt;/code&gt;. It's not really convenient to identify what test this concerns.&lt;br&gt;
We need to tweak our transaction a little bit more:&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="c1"&gt;#spec/support/config/profiling.rb&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&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;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;)&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;example&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;execution_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
      &lt;span class="n"&gt;assign_custom_transaction_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&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="o"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assign_custom_transaction_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_scope&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;scope&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_transaction_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:location&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;By using the configuration &lt;a href="https://docs.sentry.io/platforms/ruby/enriching-events/scopes/"&gt;scope&lt;/a&gt; we are able to update our transaction name. According to documentation, &lt;em&gt;"The scope will hold useful information that should be sent along with the event"&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Here we assign our test path as the transaction name. That's it.&lt;br&gt;
Run your test again and you should see&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g_D3xfI1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esl8bq3d6w1elkkrjonj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g_D3xfI1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esl8bq3d6w1elkkrjonj.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the purpose of development we didn't add a guard clause so far, but you can check if you are on your &lt;code&gt;CI&lt;/code&gt; environment before profiling your tests. &lt;/p&gt;

&lt;p&gt;The entire &lt;code&gt;profiling.rb&lt;/code&gt; class should look like this.&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="c1"&gt;#spec/support/config/profiling.rb&lt;/span&gt;

&lt;span class="no"&gt;SLOW_EXECUTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="no"&gt;MEDIUM_EXECUTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"integration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="no"&gt;SLOW_EXECUTION_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="no"&gt;MEDIUM_EXECUTION_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="no"&gt;DEFAULT_EXECUTION_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&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;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CI"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:example&lt;/span&gt;&lt;span class="p"&gt;)&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;example&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;example_start&lt;/span&gt; &lt;span class="o"&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="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;

      &lt;span class="n"&gt;example_end&lt;/span&gt; &lt;span class="o"&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="n"&gt;elapsed_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;example_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;example_start&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;execution_time_in_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         
        &lt;span class="n"&gt;assign_custom_transaction_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&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;end&lt;/span&gt;

&lt;span class="c1"&gt;# input: "./spec/integration/restaurants/owner_spec.rb:16"&lt;/span&gt;
&lt;span class="c1"&gt;# output: "integration"&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;spec_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:location&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./spec/"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;%r{^[^/]*}&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="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;threshold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&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;category&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="no"&gt;SLOW_EXECUTION&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="no"&gt;SLOW_EXECUTION_THRESHOLD&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="no"&gt;MEDIUM_EXECUTION&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="no"&gt;MEDIUM_EXECUTION_THRESHOLD&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;DEFAULT_EXECUTION_THRESHOLD&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;def&lt;/span&gt; &lt;span class="nf"&gt;assign_custom_transaction_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_scope&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;scope&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_transaction_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:location&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;That's all, happy profiling ! 🎉&lt;/p&gt;

&lt;p&gt;Interested in what we do at &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt;? Come join us! We are &lt;a href="https://jobs.lever.co/Potloc"&gt;hiring&lt;/a&gt; 🚀&lt;/p&gt;

</description>
      <category>sentry</category>
      <category>rspec</category>
      <category>profiling</category>
      <category>ruby</category>
    </item>
    <item>
      <title>OAuth Tokens &amp; Potlock gem</title>
      <dc:creator>Thibault Couraud</dc:creator>
      <pubDate>Fri, 17 Sep 2021 15:57:25 +0000</pubDate>
      <link>https://dev.to/potloc/oauth-tokens-potlock-gem-4o79</link>
      <guid>https://dev.to/potloc/oauth-tokens-potlock-gem-4o79</guid>
      <description>&lt;h2&gt;
  
  
  A bit of context 👋🏽
&lt;/h2&gt;

&lt;p&gt;When calling Apis that use OAuth as authentication process, you need to generate an &lt;strong&gt;access token&lt;/strong&gt;. And to get an access token, we have to use a &lt;strong&gt;refresh token&lt;/strong&gt; stored in the server.&lt;/p&gt;

&lt;p&gt;Here's the OAuth workflow to generate this &lt;strong&gt;access token&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4IKg56yw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://developer.ebay.com/api-docs/res/resources/images/ebay-rest/refresh_token_650x460.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4IKg56yw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://developer.ebay.com/api-docs/res/resources/images/ebay-rest/refresh_token_650x460.png" alt="https://developer.ebay.com/api-docs/res/resources/images/ebay-rest/refresh_token_650x460.png" width="650" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Image source: developer.ebay.com&lt;/em&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  So what was the need? 🤔
&lt;/h2&gt;

&lt;p&gt;An access token &lt;strong&gt;expires after a certain time&lt;/strong&gt;, in minutes, hours, days, depending on the provider. So we need to refresh it time to time.&lt;/p&gt;

&lt;p&gt;The issue was that different processes were refreshing the token at the same time, &lt;strong&gt;invalidating other's freshly generated access token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So we had to find a way to be sure that &lt;strong&gt;only one process can refresh the token&lt;/strong&gt;. &lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Here comes the gem 🚀
&lt;/h2&gt;

&lt;p&gt;Today we introduce our new gem: &lt;strong&gt;Potlock - a Distributed Read-Write lock using redis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(available on Github here: &lt;a href="https://github.com/potloc/potlock/"&gt;GitHub - potloc/potlock&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This brand new gem only allows one simultaneous reader or writer. And if the lock is taken, any readers or writers who come along will have to wait.&lt;/p&gt;

&lt;p&gt;Here's an example of how we use this gem at &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt;:&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;token&lt;/span&gt;
  &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Potlock&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;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s2"&gt;"snapchat_api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Fetch the token, refresh it if not present&lt;/span&gt;
  &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;refresh_token!&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# A token is invalid when empty or expired&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;InvalidToken&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;token&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;InvalidToken&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="c1"&gt;# Generate and save a new token&lt;/span&gt;
  &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;refresh_token!&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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 way, we are sure that all the processes will have &lt;strong&gt;the same valid access token and won't overwrite it at the same time&lt;/strong&gt; 🎉&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;em&gt;Interested in what we do at &lt;a href="https://www.potloc.com/"&gt;Potloc&lt;/a&gt;? Come join us! &lt;a href="https://jobs.lever.co/Potloc"&gt;We are hiring&lt;/a&gt; 🚀&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>oauth</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Generalized Raking for Survey Weighting</title>
      <dc:creator>Jérôme Parent-Lévesque</dc:creator>
      <pubDate>Tue, 29 Jun 2021 15:28:44 +0000</pubDate>
      <link>https://dev.to/potloc/generalized-raking-for-survey-weighting-2d1d</link>
      <guid>https://dev.to/potloc/generalized-raking-for-survey-weighting-2d1d</guid>
      <description>&lt;p&gt;In the world of surveys, it is very common that our acquired responses need to be weighted in order to achieve a sample that is &lt;em&gt;representative&lt;/em&gt; of some target population. This process of &lt;em&gt;weighting&lt;/em&gt; simply consists of assigning a &lt;em&gt;weight&lt;/em&gt; (a.k.a. &lt;em&gt;factor&lt;/em&gt;) to each respondent, and calculating all survey results as a weighted sum of respondents.&lt;/p&gt;

&lt;p&gt;For example, we might have surveyed 100 male respondents and 150 female respondents but were targeting a male / female ratio of 48% / 52%. In this simple case, we could achieve the target ratio by weighting the male responses by a factor of &lt;code&gt;0.48 / (100 / (100 + 150)) = 1.2&lt;/code&gt; and weighting the female responses by &lt;code&gt;0.52 / (150 / (100 + 150) = 0.867&lt;/code&gt;.&lt;br&gt;
The technical term for this method of computing weights is &lt;em&gt;Post-Stratification&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;However, in a more complex scenario, where we have &lt;em&gt;many&lt;/em&gt; different measurable demographic targets, how can we determine weights for all the survey respondents?&lt;/p&gt;
&lt;h2&gt;
  
  
  Raking
&lt;/h2&gt;

&lt;p&gt;At Potloc, it is very common that our clients desire survey populations matching a lot of such targets. For example, we might have targets looking like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;42% male&lt;/li&gt;
&lt;li&gt;58% female&lt;/li&gt;
&lt;li&gt;20% students&lt;/li&gt;
&lt;li&gt;80% non-students&lt;/li&gt;
&lt;li&gt;15% dog owners&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this setting, weights cannot be calculated using a simple ratio as in the male/female example shown above. Here, we instead need to rely on more involved algorithms, notably a process called &lt;strong&gt;&lt;em&gt;Raking&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Iterative Proportional Fitting
&lt;/h3&gt;

&lt;p&gt;One common approach to solve the problem of finding good weights that will satisfy our demographic targets is &lt;em&gt;Iterative Proportional Fitting&lt;/em&gt;. Typically in the industry, when the term "raking" is used it refers to this algorithm. In this method, weights for each respondents are computed &lt;strong&gt;for a single target at a time&lt;/strong&gt; using Post-Stratification. By iteratively computing this for each target and repeating a few times, the weights end up converging to values that satisfy our targets.&lt;/p&gt;

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

&lt;p&gt;...but what if we could do even better? 🤔&lt;/p&gt;
&lt;h3&gt;
  
  
  Generalized Raking
&lt;/h3&gt;

&lt;p&gt;Beyond satisfying the demographic targets, the most desirable property for the weights is that they should be as close as possible to &lt;em&gt;1&lt;/em&gt;. Indeed, weights that are really large mean that those respondents' responses will count for a lot more than the "average" respondent in our survey results. For example, a respondent with a weight of 10 will count for 10 times more than the average respondent, and 100 times more than a respondent with weight 0.1 . Similarly, small weights mean that some responses will have very little impact on the final results.&lt;/p&gt;

&lt;p&gt;Unfortunately, Iterative Proportional Fitting does nothing to encourage weights to be close to &lt;em&gt;1&lt;/em&gt;, which leads to sub-optimal weights. This is where &lt;strong&gt;&lt;em&gt;Generalized Raking&lt;/em&gt;&lt;/strong&gt;, an algorithm introduced by &lt;em&gt;Deville et al.&lt;/em&gt; (1992), comes into play.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; This is where we get into the more mathematical part of this blog post 🤓. Don't care about this part? No worries! Simply skip to the next section!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The authors of this paper formulated the weighting problem as a constrained optimization method where the objective is that the weights are as close to one as possible and where the constraint is that the targets are matched. Mathematically this looks like this:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;arg min⁡w  G(w)    s.t.  XTw=TG(x)=x(log⁡(x)−1)+1
 \argmin_{w} \; G(w) \;\; \text{s.t.} \; X^T w = T \newline
 G(x) = x(\log(x) - 1) + 1
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop"&gt;&lt;span class="mord mathrm"&gt;arg&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathrm"&gt;min&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;s.t.&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace newline"&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mop"&gt;lo&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;G(x)G(x)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the &lt;em&gt;raking&lt;/em&gt; function which encourages weights to be close to &lt;em&gt;1&lt;/em&gt;, 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;ww&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the vector of weights, 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;TT&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the vector of targets (in absolute numbers, not percentages) and 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;XX&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;(numRespondents×numTargets)(\text{numRespondents} \times \text{numTargets})&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;numRespondents&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;numTargets&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 matrix of responses. The matrix 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;XX&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is binary where cells are filled with a '1' if the respondent belongs to the target category and '0' otherwise.&lt;/p&gt;

&lt;p&gt;In other words, this is saying that we want to optimize the weights to be as close to 1 as possible while satisfying the target constraints. This is achieved by minimizing the function 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;G(x)G(x)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 which looks like this (notice the global minimum at 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;x=1x=1&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
!):&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3nsmhkkonuiknwa4h8dj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3nsmhkkonuiknwa4h8dj.png" alt="image" width="457" height="285"&gt;&lt;/a&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  The Generalized Raking Algorithm
&lt;/h2&gt;

&lt;p&gt;While it is possible to solve this optimization problem using general methods such as &lt;a href="https://docs.scipy.org/doc/scipy/reference/optimize.minimize-slsqp.html" rel="noopener noreferrer"&gt;Sequential Least Squares Programming&lt;/a&gt;, the authors of Generalized Raking have devised a more efficient and robust algorithm for this specific problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Initialize variables

&lt;ul&gt;
&lt;li&gt;A 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;(numRespondents×1)(\text{numRespondents} \times 1)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;numRespondents&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 vector 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;ww&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 to ones&lt;/li&gt;
&lt;li&gt;A 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;(numTargets×1)(\text{numTargets} \times 1)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;numTargets&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 vector 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;λ\lambda&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 to zeros&lt;/li&gt;
&lt;li&gt;A 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;(numRespondents×numRespondents)(\text{numRespondents} \times \text{numRespondents})&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;numRespondents&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;numRespondents&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 square matrix 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;HH&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 to the Identity matrix&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;While the weights have not converged, repeat:

&lt;ol&gt;
&lt;li&gt;
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;λ=λ+(XTHX)−1(T−XTw)\lambda = \lambda + (X^T H X)^{-1} (T - X^T w)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;w=G−1(Xλ)w = G^{-1}(X \lambda)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;H=diag(G−1′(Xλ))H = \text{diag}({G^{-1}}'(X \lambda))&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;diag&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="mclose"&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here, 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;G−1(x)G^{-1}(x)&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the inverse of the derivative of the raking function, i.e. 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;exe^x&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
.&lt;br&gt;

&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;G−1′{G^{-1}}'&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is its derivative, in this case also 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;exe^x&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
.&lt;/p&gt;

&lt;p&gt;The final value of 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;ww&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 corresponds to the weighting factors we are looking for!&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;While there are many implementations of this algorithm in R, we were not able to find one in Ruby that could play well with our codebase and be easily maintainable.&lt;br&gt;
We therefore decided to make our own and to share it here for anyone looking for something similar. We started by making an implementation in python with the popular &lt;code&gt;numpy&lt;/code&gt; library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;d_raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;graking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tolerance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="c1"&gt;# Based on algo in (Deville et al., 1992) explained in detail on page 37 in
&lt;/span&gt;  &lt;span class="c1"&gt;# https://orca.cf.ac.uk/109727/1/2018daviesgpphd.pdf
&lt;/span&gt;
  &lt;span class="c1"&gt;# Initialize variables - Step 1
&lt;/span&gt;  &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;
  &lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Lagrange multipliers (lambda)
&lt;/span&gt;  &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Our weights (will get progressively updated)
&lt;/span&gt;  &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eye&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pinv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="c1"&gt;# Step 2.1
&lt;/span&gt;    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Step 2.2
&lt;/span&gt;    &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;d_raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="c1"&gt;# Step 2.3
&lt;/span&gt;
    &lt;span class="c1"&gt;# Termination condition:
&lt;/span&gt;    &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;tolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Did not converge&lt;/span&gt;&lt;span class="sh"&gt;"&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;w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ruby Implementation
&lt;/h3&gt;

&lt;p&gt;After validating the algorithm in python, we then proceeded to replicate it in Ruby. For this, we had to find an equivalent to &lt;code&gt;numpy&lt;/code&gt; which we found in &lt;a href="https://github.com/ruby-numo" rel="noopener noreferrer"&gt;Numo&lt;/a&gt;. Numo is an awesome library for vector and matrix operations, and its &lt;a href="https://github.com/ruby-numo/numo-linalg" rel="noopener noreferrer"&gt;&lt;code&gt;linalg&lt;/code&gt;&lt;/a&gt; sub-library was perfect for us as we needed to compute a matrix pseudo-inverse. This allowed us to translate the code to Ruby almost line by line:&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="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"numo/narray"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"numo/linalg"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Numo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NMath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;d_raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Numo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NMath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;graking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;X&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="ss"&gt;max_steps: &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tolerance: &lt;/span&gt;&lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Based on algo in (Deville et al., 1992) explained in detail on page 37 in&lt;/span&gt;
  &lt;span class="c1"&gt;# https://orca.cf.ac.uk/109727/1/2018daviesgpphd.pdf&lt;/span&gt;

  &lt;span class="c1"&gt;# Initialize variables - Step 1&lt;/span&gt;
  &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shape&lt;/span&gt;
  &lt;span class="no"&gt;L&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Numo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Numo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;H_diag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Numo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;

  &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;L&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="no"&gt;Numo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pinv&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;H_diag&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Step 2.1&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Step 2.2&lt;/span&gt;
    &lt;span class="no"&gt;H_diag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d_raking_inverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Step 2.3&lt;/span&gt;

    &lt;span class="c1"&gt;# Termination condition:&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="no"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt; &lt;span class="o"&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;max&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;tolerance&lt;/span&gt;
      &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;break&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;raise&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Did not converged"&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;
  &lt;span class="n"&gt;w&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may have noticed that the code doesn't quite match exactly the algorithm described above, notably steps 2.1 and 2.3. This is because we have found it to be vastly faster with Numo to store the sparse matrix 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;HH&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 as a flat vector &lt;code&gt;h_matrix_diagonal&lt;/code&gt; since it only contains values on the diagonal. As a result, the step of taking the product 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;XTHX^T H&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;X&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 can be rewritten as &lt;code&gt;X.Transpose * h_matrix_diagonal&lt;/code&gt;, making use of Numo's implicit broadcasting.&lt;/p&gt;

&lt;p&gt;In practice, we optimize this code a bit further by exiting early whenever possible (for example if our loss becomes &lt;code&gt;NaN&lt;/code&gt;) and by allowing to pass as input an initial value for the vector &lt;code&gt;lambdas&lt;/code&gt; if we believe to have an initialisation value better than the default.&lt;/p&gt;

&lt;p&gt;With these few lines of code, we are now able to support complex survey weighting scenarios while having all of our code in our beautiful Ruby monolith 🎉&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interested in what we do at Potloc? Come join us! &lt;a href="https://jobs.lever.co/Potloc?team=Engineering" rel="noopener noreferrer"&gt;We are hiring&lt;/a&gt; 🚀&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://orca.cf.ac.uk/109727/1/2018daviesgpphd.pdf" rel="noopener noreferrer"&gt;Gareth Davies' excellent PhD Thesis on the subject&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jstor.org/stable/2290793" rel="noopener noreferrer"&gt;Deville et al.'s 1992 &lt;em&gt;Generalized Raking Procedures&lt;/em&gt; paper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>python</category>
      <category>statistics</category>
    </item>
  </channel>
</rss>
