<?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: Leonardo Holanda</title>
    <description>The latest articles on DEV Community by Leonardo Holanda (@leoholanda).</description>
    <link>https://dev.to/leoholanda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1140759%2Fc030b175-e35e-4501-8a4b-dda982f5a908.jpg</url>
      <title>DEV Community: Leonardo Holanda</title>
      <link>https://dev.to/leoholanda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leoholanda"/>
    <language>en</language>
    <item>
      <title>O que 2.312 vagas pra devs na Gupy dizem sobre o mercado de vagas?</title>
      <dc:creator>Leonardo Holanda</dc:creator>
      <pubDate>Sat, 16 Mar 2024 12:37:44 +0000</pubDate>
      <link>https://dev.to/leoholanda/o-que-2312-vagas-pra-devs-na-gupy-dizem-sobre-o-mercado-de-vagas-2j27</link>
      <guid>https://dev.to/leoholanda/o-que-2312-vagas-pra-devs-na-gupy-dizem-sobre-o-mercado-de-vagas-2j27</guid>
      <description>&lt;p&gt;Após quase 3 meses, 2.312 vagas pra devs da Gupy foram coletadas pelo vagômetro, um rastreador de vagas de TI no Brasil.&lt;/p&gt;

&lt;p&gt;Ao chegar nessa marca, pensei em fazer uma postagem compartilhando um resumo dos dados.&lt;/p&gt;

&lt;p&gt;Dado que o vagômetro dá 13 informações diferentes sobre as vagas e as possibilidades de composições são muitas, eu optei por apresentar apenas três principais pontos e, para cada ponto, três observações que julgo interessantes.&lt;/p&gt;

&lt;p&gt;Caso você queria validar os dados aqui citados ou vê-los em sua totalidade, acesse o vagômetro no link &lt;a href="https://vagometro.vercel.app/" rel="noopener noreferrer"&gt;https://vagometro.vercel.app/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Os dados utilizados nesse texto vão até o dia 16/03/2024.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vagas de nível Júnior representam apenas 7% do total
&lt;/h2&gt;

&lt;p&gt;Isso dá 160 vagas. Do total de 2.312, a maior parte se divide entre:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;867 vagas pra sênior (38%) &lt;/li&gt;
&lt;li&gt;579 vagas pra pleno (25%)&lt;/li&gt;
&lt;li&gt;424 não informam o nível mas pedem experiência (18%)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tecnologias mais requisitadas em vagas pra júnior
&lt;/h3&gt;

&lt;p&gt;Dessas 160 vagas, as linguagens mais requisitadas são:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Java&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Outros termos que receberam bastante menção foram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testes&lt;/li&gt;
&lt;li&gt;API&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Agile&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Modalidades mais frequentes em vagas pra júnior
&lt;/h3&gt;

&lt;p&gt;A divisão de modalidades foi bastante equilibrada se considerarmos presencial e híbrido como uma categoria só.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;81 vagas remotas (51%)&lt;/li&gt;
&lt;li&gt;47 vagas híbridas (29%)&lt;/li&gt;
&lt;li&gt;32 vagas presenciais (20%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para as vagas presenciais/híbridas, as três cidades que mais ofertam vagas foram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;São Paulo&lt;/li&gt;
&lt;li&gt;Rio de Janeiro&lt;/li&gt;
&lt;li&gt;Barueri/SP&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Repostagens das vagas pra júnior
&lt;/h3&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%2Fhk61u6wi1b2h1hyqisz0.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%2Fhk61u6wi1b2h1hyqisz0.png" alt="Repostagens das vagas pra júnior" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;99% das vagas não foram repostadas nenhuma vez. Uma interpretação plausível é de que não há dificuldade em fechar as vagas de nível júnior, logo não há necessidade em repostar.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript, Java e SQL no topo
&lt;/h2&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%2Fmme490r3p3oumyx8fhoz.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%2Fmme490r3p3oumyx8fhoz.png" alt="Ranking de tecnologias" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Considerando todas as 2.312 vagas, as linguagens que mais apareceram nas vagas foram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;860 vagas (30%)&lt;/li&gt;
&lt;li&gt;3ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;777 vagas (34%)&lt;/li&gt;
&lt;li&gt;6ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;SQL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;667 vagas (29%)&lt;/li&gt;
&lt;li&gt;8ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Termos relacionados à bancos de dados mais requisitados
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Oracle&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;328 vagas (14%)&lt;/li&gt;
&lt;li&gt;23ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;MongoDB&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;273 vagas (12%) &lt;/li&gt;
&lt;li&gt;28ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;MySQL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;242 vagas (11%) &lt;/li&gt;
&lt;li&gt;30ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Panorama do Backend
&lt;/h3&gt;

&lt;p&gt;Para as tecnologias que podem ser utilizadas no backend, a divisão ficou da seguinte forma:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;777 vagas (34%) &lt;/li&gt;
&lt;li&gt;6ª posição no ranking.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;O Spring Boot é o framework de Java mais citado com 418 vagas (18%) estando na 20ª posição no ranking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; (JavaScript ou TypeScript)

&lt;ul&gt;
&lt;li&gt;518 vagas (22%)&lt;/li&gt;
&lt;li&gt;14ª posição no ranking.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;NestJS é o framework de Node.js mais citado com 58 vagas (3%) estando na 90ª posição no ranking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C#&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;302 vagas (13%) &lt;/li&gt;
&lt;li&gt;25ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;.NET é o framework de C# mais citado com 112 vagas (5%) estando na 57ª posição no ranking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Termos Gerais
&lt;/h3&gt;

&lt;p&gt;Outros termos gerais que ganharam bastante menção foram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Testes&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.159 (50%)&lt;/li&gt;
&lt;li&gt;1ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;API&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;907 vagas (39%)&lt;/li&gt;
&lt;li&gt;2ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Git&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;729 vagas (34%)&lt;/li&gt;
&lt;li&gt;4ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Agile&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;791 vagas (34%)&lt;/li&gt;
&lt;li&gt;5ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;REST&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;687 vagas (30%)&lt;/li&gt;
&lt;li&gt;7ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Testes unitários&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;656 vagas (28%)&lt;/li&gt;
&lt;li&gt;9ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Scrum&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;581 vagas (28%)&lt;/li&gt;
&lt;li&gt;10ª posição no ranking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Seriam esses os conhecimentos mais valorizados pelo mercado? Comente sua opinião!&lt;/p&gt;

&lt;h2&gt;
  
  
  Vagas remotas ainda são mais da metade
&lt;/h2&gt;

&lt;p&gt;A divisão das vagas por modalidade ficou assim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Remoto&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.293 vagas (56%)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Híbrido&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;702 vagas (30%)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Presencial&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;317 vagas (14%)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Experiência é valorizada em vagas remotas
&lt;/h3&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%2Fayzvajczwn13hef97qdn.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%2Fayzvajczwn13hef97qdn.png" alt="Ranking de experiência em vagas remotas" width="800" height="689"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vagas pra sênior e pleno somam 898 vagas (69%) enquanto pra júnior há 81 vagas (6%).&lt;/p&gt;

&lt;h3&gt;
  
  
  Equilíbrio em vagas presenciais
&lt;/h3&gt;

&lt;p&gt;A diferença entre níveis de experiência é minimizada em vagas presenciais.&lt;/p&gt;

&lt;p&gt;Nessa modalidade, foram postadas 50 vagas sênior (16%), 53 vagas pleno (17%) e 32 vagas júnior (10%). O primeiro lugar, cujo nível é desconhecido mas a vaga pede experiência, teve 87 vagas (27%).&lt;/p&gt;

&lt;p&gt;Seria uma forma das empresas compensarem a modalidade presencial com uma exigência de experiência mais flexível? &lt;/p&gt;

&lt;h3&gt;
  
  
  Vagas híbridas mantém desequilíbrio
&lt;/h3&gt;

&lt;p&gt;445 é o total de vagas pra sênior e pleno (63%) que ocupam o primeiro e segundo lugar no ranking.&lt;/p&gt;

&lt;p&gt;Vagas pra júnior ficam em quarto lugar com 47 vagas (7%).&lt;/p&gt;

&lt;h2&gt;
  
  
  Mais dados
&lt;/h2&gt;

&lt;p&gt;O vagômetro coleta diariamente vagas da Gupy e LinkedIn, além de possuir vagas de repositórios GitHub que vão desde de 2016.&lt;/p&gt;

&lt;p&gt;Toda vaga é mapeada para encontrar informações como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tecnologias requisitadas (JavaScript, Docker, Figma, TensorFlow, etc)&lt;/li&gt;
&lt;li&gt;Modalidade da vaga (remoto, híbrido ou presencial)&lt;/li&gt;
&lt;li&gt;Tipo de contrato (CLT, PJ, Estágio, etc)&lt;/li&gt;
&lt;li&gt;Nível de experiência (Sênior, Pleno, Júnior, etc)&lt;/li&gt;
&lt;li&gt;Inclusão da vaga (Afirmativa para pessoas negras, mulheres, PCD, etc)&lt;/li&gt;
&lt;li&gt;Nível educacional (Graduação, mestrado, ensino médio, etc)&lt;/li&gt;
&lt;li&gt;Idiomas requisitados (Inglês, espanhol, etc)&lt;/li&gt;
&lt;li&gt;Certificações requisitadas&lt;/li&gt;
&lt;li&gt;Quais cidades ofertam mais vagas&lt;/li&gt;
&lt;li&gt;Quais empresas postam mais vagas&lt;/li&gt;
&lt;li&gt;Quantas vezes a vaga foi repostada&lt;/li&gt;
&lt;li&gt;Quanto tempo se passou entre as repostagens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Além disso, você ainda pode criar um perfil de busca que será comparado com todas as vagas, gerando uma porcentagem de match para cada uma delas e facilitando a busca por vagas com mais aderência ao seu perfil.&lt;/p&gt;

&lt;p&gt;Todas as vagas utilizadas nas análises estão listadas perto do rodapé das páginas junto com o link de sua postagem.&lt;/p&gt;

&lt;p&gt;O vagômetro é um projeto open-source. Para conferir o repositório, acesse o link: &lt;a href="https://github.com/leo-holanda/vagometro" rel="noopener noreferrer"&gt;https://github.com/leo-holanda/vagometro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Para conferir o vagômetro, acesse o link &lt;a href="https://vagometro.vercel.app/" rel="noopener noreferrer"&gt;https://vagometro.vercel.app/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vagas</category>
      <category>gupy</category>
      <category>emprego</category>
    </item>
    <item>
      <title>How LogCharts Was Developed</title>
      <dc:creator>Leonardo Holanda</dc:creator>
      <pubDate>Thu, 02 Nov 2023 19:16:00 +0000</pubDate>
      <link>https://dev.to/leoholanda/how-logcharts-was-developed-5eo7</link>
      <guid>https://dev.to/leoholanda/how-logcharts-was-developed-5eo7</guid>
      <description>&lt;p&gt;Using HWInfo logs can be really useful to diagnose problems in your hardware. But a .csv file isn't the most human-readable thing and neither is loading it in Excel or LibreOffice Calc.&lt;/p&gt;

&lt;p&gt;In this post, I'm going to talk more about &lt;a href="https://logcharts-io.pages.dev/" rel="noopener noreferrer"&gt;LogCharts&lt;/a&gt;, a tool I made to make HWInfo logs visualization more human-friendly.&lt;/p&gt;

&lt;p&gt;Here's what you will see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Problem&lt;/li&gt;
&lt;li&gt;The Solution&lt;/li&gt;
&lt;li&gt;The Implementation&lt;/li&gt;
&lt;li&gt;Hosting and Analytics&lt;/li&gt;
&lt;li&gt;The Result&lt;/li&gt;
&lt;li&gt;What I learn from all this&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You just downloaded HWInfo to find out how your computer hardware behaves and why it behaves the way it does.&lt;/p&gt;

&lt;p&gt;You noticed that opening HWInfo just collects the min, max, current and average data. But you need something that shows the data over time.&lt;/p&gt;

&lt;p&gt;You discover that HWInfo has a log feature that exports a .csv file containing all the sensor data that were collected between when you activated the feature and deactivated it.&lt;/p&gt;

&lt;p&gt;You open the .csv file and this is what it looks like:&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%2Fnb7dtqv20b9vyn27cl6l.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%2Fnb7dtqv20b9vyn27cl6l.png" alt="A .csv file opened in VS Code" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It has what you need but you can't exactly interpret it, right? You decide to load it into a table and it looks like this:&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%2F50ychqrh5bvlvdc1fy8y.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%2F50ychqrh5bvlvdc1fy8y.png" alt="A .csv file loaded into a table" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's getting better. Now you can sort, filter and things like that. But what about the relationship between your data and time? It's kinda hard to analyse it through a table, isn't it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;What if it was possible to load the .csv and generate line charts showing how your data spreads over time?&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%2Fk75v172fmivu6bxqberl.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%2Fk75v172fmivu6bxqberl.png" alt="A screenshot of LogCharts chart screen" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's what LogCharts does. Now, you can see logs the way they are meant to be seen. And with some nice additions like the tooltip and the brush.&lt;/p&gt;

&lt;p&gt;Since you may want to compare data from different sensors, you can create as many lines as you want and select which data they will show while assigning colours to them.&lt;/p&gt;

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

&lt;p&gt;I made this project when I was learning the web development basics: HTML, CSS and JavaScript. It feels like ages back to the time when I developed it so I don't remember all the details, unfortunately.&lt;/p&gt;

&lt;p&gt;I do remember that I made some poor decisions that made the code harder to maintain. At the time, I thought: "I have an HTML page. I'm gonna create lots of JS files to manipulate this HTML page and lots of style files to style it. And that's it".&lt;/p&gt;

&lt;p&gt;Some time ago I needed to get back to the code to fix a bug and it took me some time to understand where the things were and what I should change to fix the bug.&lt;/p&gt;

&lt;p&gt;This helped me learn how componentization helps isolate things and how valuable this is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting and Analytics
&lt;/h2&gt;

&lt;p&gt;After years of hosting LogCharts on GitHub pages, I got curious to see how many people were using it. For this, I needed an analytics solution that GitHub Pages doesn't provide.&lt;/p&gt;

&lt;p&gt;After searching for options, I opted for Cloudflare Web Analytics. It doesn't have the cookie banner thing that I find pretty annoying while also working nicely and being easy to deploy.&lt;/p&gt;

&lt;p&gt;It helped me find that LogCharts has nearly 280 monthly visits! It ain't much but it's honest work.&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%2Fnyi1zprb8k9j1w3t07dx.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%2Fnyi1zprb8k9j1w3t07dx.png" width="800" height="625"&gt;&lt;/a&gt;&lt;br&gt;Cloudflare Web Analytics report for LogCharts&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;I also used the project to learn Docker. I created a Dockerfile that runs an NGNIX server that serves LogCharts static files. Someone even created an issue asking me for a docker-compose file which was nice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;You can check LogCharts &lt;a href="https://logcharts-io.pages.dev/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. And its source code &lt;a href="https://github.com/leo-holanda/logcharts.io" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from all this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to use D3.js to create interactive line charts&lt;/li&gt;
&lt;li&gt;How to work with .csv files&lt;/li&gt;
&lt;li&gt;How to deploy a project to GitHub Pages and Cloudflare&lt;/li&gt;
&lt;li&gt;How to create a GitHub Action&lt;/li&gt;
&lt;li&gt;How to use ESBuild to create bundles for production&lt;/li&gt;
&lt;li&gt;How to use Docker in a project&lt;/li&gt;
&lt;li&gt;How componentization can increase maintainability in a project&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>The VS Code Extension That Helps You Control Your Pull Requests Size</title>
      <dc:creator>Leonardo Holanda</dc:creator>
      <pubDate>Wed, 01 Nov 2023 20:11:53 +0000</pubDate>
      <link>https://dev.to/leoholanda/an-vs-code-extension-that-helps-you-control-your-pull-requests-size-4pe4</link>
      <guid>https://dev.to/leoholanda/an-vs-code-extension-that-helps-you-control-your-pull-requests-size-4pe4</guid>
      <description>&lt;p&gt;When I was working as an intern developer in a startup in my hometown, I noticed one particular thing that was quite annoying: large pull requests.&lt;/p&gt;

&lt;p&gt;In this post, I'm gonna talk more about &lt;a href="https://marketplace.visualstudio.com/items?itemName=LeonardoHolanda.changes-counter" rel="noopener noreferrer"&gt;Changes Counter&lt;/a&gt;, a VS Code extension I made to tackle this problem. Here's what you will see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Problem&lt;/li&gt;
&lt;li&gt;The Solution&lt;/li&gt;
&lt;li&gt;The Implementation&lt;/li&gt;
&lt;li&gt;The Result&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you have reviewed a PR with thousands of lines, you know that it ain't fun. It takes time and the more time you spend on it, the more tired you get while also becoming harder to find bugs and mistakes in the code.&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%2Fwww.swarmia.com%2Fstatic%2Fdcad6608994a7c2fcca0ab325233fa17%2F27330%2Flgtm-tweet.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%2Fwww.swarmia.com%2Fstatic%2Fdcad6608994a7c2fcca0ab325233fa17%2F27330%2Flgtm-tweet.png" width="590" height="323"&gt;&lt;/a&gt;&lt;br&gt;This tweet is a classic when posts are made about pull requests&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;In the context of my internship, I remember seeing some large PRs taking so long to be merged that it would affect the progress of other tasks resulting in bottlenecks.&lt;/p&gt;

&lt;p&gt;And there's actually a &lt;a href="https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/" rel="noopener noreferrer"&gt;study&lt;/a&gt; on this made by a company called SmartBear which recommends reviewing "no more than 200 to 400 lines of code at a time".&lt;/p&gt;

&lt;p&gt;In summary: large pull requests suck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why would they happen in my team?
&lt;/h3&gt;

&lt;p&gt;During the Scrum ceremonies, our team would decide which histories and tasks to create on the Jira board. Most of the time, we would agree that the tasks' scope was fine and reasonable. However, when working on these tasks, we noticed that they required more changes than predicted.&lt;/p&gt;

&lt;p&gt;Sometimes, we would push the changes to the remote branch and create pull requests with lots of changes. Sometimes, we would notice that it would be too much for our reviewer friend and split the task into two separate ones.&lt;/p&gt;

&lt;p&gt;But this whole process would rely on intuition rather than the actual number of changes that we would make in the PR. And being a team of interns, this intuition hasn't yet got enough time to develop itself and achieve good results. The same thing applies to task creation, I think.&lt;/p&gt;

&lt;p&gt;So I thought: What if the dev would know exactly how many changes will go in the PR while coding the task instead of relying on intuition?&lt;/p&gt;

&lt;p&gt;This way, the dev will always know if the PR is getting larger than desired and then decide to take some action.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;One of the extensions that we were encouraged to use in the internship was GitLens. It provides lots of features to make the whole git experience better and it actually would come in handy frequently during the work we did.&lt;/p&gt;

&lt;p&gt;While using it, I noticed that the Search &amp;amp; Compare feature contains data about changed lines of code. But unfortunately, not in a way that could solve our problem.&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%2Fa5zypzec88m879kjgu52.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%2Fa5zypzec88m879kjgu52.png" width="436" height="345"&gt;&lt;/a&gt;&lt;br&gt;GitLens Search &amp;amp; Compare feature screenshot&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;To solve our problem, the data needed to be presented in an easier way to look at while being more useful to the developer during the process of working on a task. Kinda like a status bar item like the Errors &amp;amp; Warnings.&lt;/p&gt;

&lt;p&gt;Also, a notification warning the developer that a given change quantity threshold was exceeded could be nice in case the dev was unaware of it.&lt;/p&gt;

&lt;p&gt;I ended up with these requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A status bar item that shows how many lines of code were changed&lt;/li&gt;
&lt;li&gt;The item is updated every time a file is saved&lt;/li&gt;
&lt;li&gt;The user can set a threshold to determine an acceptable quantity of changed lines&lt;/li&gt;
&lt;li&gt;A notification is sent when the threshold is exceeded&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;With the requirements in mind, I decided to develop a VS Code extension myself and try to tackle these requirements into features.&lt;/p&gt;

&lt;p&gt;I knew nothing about coding VS Code extensions. However, this &lt;a href="https://code.visualstudio.com/api/get-started/your-first-extension" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; was really valuable in giving the base extension code and instructions on running it in a dev environment.&lt;/p&gt;

&lt;p&gt;Then, it was a matter of adding the features I wanted. Lots of things were actually simple and I don't think it's worth mentioning here. But this one problem was interesting to me:&lt;/p&gt;

&lt;h3&gt;
  
  
  How to run Git commands in TypeScript?
&lt;/h3&gt;

&lt;p&gt;The first question I had was: "How to count the number of lines changed?". Since I already knew about git diff, it was just a matter of running this command and working on its output.&lt;/p&gt;

&lt;p&gt;But since VS Code extensions are developed with TypeScript in a Node.js environment, how would it be possible to run git diff and hold its output inside the extension code?&lt;/p&gt;

&lt;p&gt;Searching about it, I discovered the Child Process module from Node.js. You can use it to spawn a subprocess that can run git commands. This is the code provided in the &lt;a href="https://nodejs.org/docs/latest-v18.x/api/child_process.html#child_processspawncommand-args-options" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) =&amp;gt; {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) =&amp;gt; {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) =&amp;gt; {
  console.log(`child process exited with code ${code}`);
}); 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bringing this to the extension code, we get something 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;  async getDiffData(): Promise&amp;lt;DiffData&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
      const comparisonBranch = this.context.workspaceState.get&amp;lt;string&amp;gt;("comparisonBranch");
      if (comparisonBranch === undefined) {
        reject("A comparison branch wasn't defined. Please, define a comparison branch.");
        return;
      }

      const gitChildProcess = spawn(
        "git",
        ["diff", comparisonBranch, "--shortstat", ...this.diffExclusionParameters],
        {
          cwd: vscode.workspace.workspaceFolders![0].uri.fsPath,
          shell: true, // Diff exclusion parameters doesn't work without this
        }
      );

      gitChildProcess.on("error", (err) =&amp;gt; reject(err));

      let chunks: Buffer[] = [];
      gitChildProcess.stdout.on("data", (chunk: Buffer) =&amp;gt; {
        chunks.push(chunk);
      });

      gitChildProcess.stderr.on("data", (data: Buffer) =&amp;gt; {
        reject(data.toString());
      });

      gitChildProcess.on("close", () =&amp;gt; {
        const processOutput = Buffer.concat(chunks);
        resolve(this.extractDiffData(processOutput));
      });
    });
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By calling this function, we get all we need through the resolved promise: an object containing the changes data or the error we must handle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging
&lt;/h3&gt;

&lt;p&gt;A thing I learned while developing this extension is how important logging is. When some users reported problems, the lack of information was something that made debugging difficult.&lt;/p&gt;

&lt;p&gt;When I started logging some lifecycle events and errors that could eventually appear, I noticed that it would be far easier to debug once the user sent me the log. Knowing where to search for the bug is obviously crucial.&lt;/p&gt;

&lt;p&gt;If you are developing VS Code extensions, try to introduce logging as early as possible to avoid debugging in the dark.&lt;/p&gt;

&lt;p&gt;And don't forget to search for the best practices before doing it. They are simple and can be helpful to make your logging more consistent with other applications. Here's an &lt;a href="https://devcenter.heroku.com/articles/writing-best-practices-for-application-logs" rel="noopener noreferrer"&gt;example&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;You can see the extension page at the Visual Studio Marketplace &lt;a href="https://marketplace.visualstudio.com/items?itemName=LeonardoHolanda.changes-counter" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&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%2Fmedia2.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExaHE2eWduY3oxaHUxcmRoOGpubHQ3MGM2aDRlYmJsNmRvODNmcDZtMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FAmNBg5C7EyT9APOSUY%2Fgiphy.gif" 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%2Fmedia2.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExaHE2eWduY3oxaHUxcmRoOGpubHQ3MGM2aDRlYmJsNmRvODNmcDZtMCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FAmNBg5C7EyT9APOSUY%2Fgiphy.gif" width="480" height="262"&gt;&lt;/a&gt;&lt;br&gt;Changes Counter extension&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;And you can see the code &lt;a href="https://github.com/leo-holanda/changes-counter/blob/main/src/extension.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Any feedback and suggestions are more than welcome!&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from all this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to create and deploy a VS Code extension&lt;/li&gt;
&lt;li&gt;How to spawn subprocesses and run system commands with the Child Process module from Node.js&lt;/li&gt;
&lt;li&gt;The importance of logging&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>github</category>
      <category>vscode</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How To Find An Artist's Country of Origin?</title>
      <dc:creator>Leonardo Holanda</dc:creator>
      <pubDate>Mon, 30 Oct 2023 13:32:46 +0000</pubDate>
      <link>https://dev.to/leoholanda/how-to-find-an-artists-country-of-origin-546k</link>
      <guid>https://dev.to/leoholanda/how-to-find-an-artists-country-of-origin-546k</guid>
      <description>&lt;p&gt;I'm this post, I'm gonna talk more about the approach I used to find an artist's country of origin.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quick note before we start
&lt;/h3&gt;

&lt;p&gt;For this post, I thought that showing the problems I solved, the solutions I found and the mistakes I made would be more interesting than just showing the code and explaining it. If you want to replicate it, at least you know what not to do. What do you think?&lt;/p&gt;

&lt;p&gt;Also, my goal with this series of posts is to share what I learned while developing Cartogrify so I thought it would make more sense.&lt;/p&gt;

&lt;p&gt;Anyway, if you just want to see the code, it's near the end.&lt;/p&gt;

&lt;p&gt;In this post, we will see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Problem&lt;/li&gt;
&lt;li&gt;Where to fetch the data?&lt;/li&gt;
&lt;li&gt;How to fetch the data?&lt;/li&gt;
&lt;li&gt;The First Solution&lt;/li&gt;
&lt;li&gt;The Second Solution&lt;/li&gt;
&lt;li&gt;The Final Solution&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Cartogrify fetches the user's 50 top artists from Spotify or Last.fm APIs. In both of them, you will have an array of objects containing the artists' data which includes their names.&lt;/p&gt;

&lt;p&gt;To generate the data visualization, you need to know where the artists come from or know that you couldn't find their countries. The aim is to end up with an array of objects containing the artists' names and the country where they come from or undefined.&lt;/p&gt;

&lt;p&gt;Since the country detection algorithm spends the hosting free tier resources, it shouldn't run for an artist that it already encountered before. Because of this, every searched artist needs to be saved in a database for future queries.&lt;/p&gt;

&lt;p&gt;Also, it's kinda boring to look at a spinner and wait for 20+ artists to have their countries discovered. Since it can take a while, I did this loading screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3N2czZ5NzR1b2VubWlzNTN6cm8xdG1pYWVxbnRncjNsdGp2ejlvcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/yGrvWMTcjTN73bHNGW/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3N2czZ5NzR1b2VubWlzNTN6cm8xdG1pYWVxbnRncjNsdGp2ejlvcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/yGrvWMTcjTN73bHNGW/giphy.gif" width="480" height="262"&gt;&lt;/a&gt;&lt;br&gt;Cartogrify country detection loading screen&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;It means that the artists must have their country detected sequentially rather than wait for all of them to be detected to proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to fetch the data?
&lt;/h2&gt;

&lt;p&gt;I already knew by looking at &lt;a href="https://github.com/mold/explr" rel="noopener noreferrer"&gt;explr.fm source code&lt;/a&gt; that using Last.fm API was an option.&lt;/p&gt;

&lt;p&gt;However, I didn't want to follow the devs' approach to extract country data from the artist's tags since it's an unreliable source. Sometimes, there's country tags and sometimes not. So I went searching for alternatives.&lt;/p&gt;

&lt;p&gt;While I was searching, I stumbled upon Dr. Markus Schedl's paper &lt;a href="https://dl.acm.org/doi/abs/10.1145/1835449.1835623" rel="noopener noreferrer"&gt;"Three web-based heuristics to determine a person's or institution's country of origin"&lt;/a&gt;. Dr. Schedl's approach relies on using a search engine with a specific query to retrieve top-ranked pages and extract the person's country data from their textual content.&lt;/p&gt;

&lt;p&gt;This approach might work well in a research environment but I'm not quite sure about a web environment. The Google Custom Search API limit of 100 search queries for free per day seems heavily restrictive.&lt;/p&gt;

&lt;p&gt;However, the article also mentions that other authors use a different approach by fetching data directly from specific websites. This approach is more suitable for web environments due to less usage restrictions which is the reason I chose it in Cartogrify.&lt;/p&gt;

&lt;p&gt;You could also just ask ChatGPT. I did some tests and the answers were correct most of the time. But since it isn't free, it isn't an option for me, unfortunately.&lt;/p&gt;

&lt;p&gt;Besides Last.fm, I searched for more websites that would contain artists' country data. These were the ones I found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate Your Music&lt;/li&gt;
&lt;li&gt;Discogs&lt;/li&gt;
&lt;li&gt;MediaWiki API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They ended up being the initial "source pool".&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fetch the data?
&lt;/h2&gt;

&lt;p&gt;A great thing about some of these websites is that they have public APIs where you can send requests and get data about artists.&lt;/p&gt;

&lt;p&gt;There are only two options, then: Send a request to the music website API or go to the artist profile page and do web scraping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which source to choose?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Rate Your Music ❌
&lt;/h4&gt;

&lt;p&gt;Rate Your Music doesn't have a public API and it blocked my IP when I sent a request to an artist profile page. So neither of the options is available.&lt;/p&gt;

&lt;h4&gt;
  
  
  Discogs ❌
&lt;/h4&gt;

&lt;p&gt;Discogs do have a public API but the artist search endpoint response doesn't have country data. &lt;/p&gt;

&lt;p&gt;Since they are heavily focused on albums, the only country data available is related to albums. But I suppose is the country where the album was produced so it isn't reliable.&lt;/p&gt;

&lt;p&gt;Web scraping, according to some forum posts, can also result in an IP ban.&lt;/p&gt;

&lt;h4&gt;
  
  
  MediaWiki API ❌
&lt;/h4&gt;

&lt;p&gt;While taking a deep look at MediaWiki, which is under the Wikipedia umbrella, I noticed that it contains data for the most famous artists but the underground ones are missing.&lt;/p&gt;

&lt;p&gt;Because of the equivalence between the data from Last.fm and MediaWiki for famous artists and Last.fm giving better results for underground ones, I decided to stick with Last.fm.&lt;/p&gt;

&lt;h4&gt;
  
  
  Last.fm ✅
&lt;/h4&gt;

&lt;p&gt;Last.fm has a public API but the country data may only be available indirectly through tags or wiki text.&lt;/p&gt;

&lt;p&gt;They do allow web scraping and the artist's page can be reached using the artist's name. Most famous artists have their country available on their pages which makes web scraping a reliable option.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Solution
&lt;/h2&gt;

&lt;p&gt;Given this context, I have chosen the web scraping approach using Last.fm as a source. It seemed like a step up from the explr.fm approach so I dived into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caveats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CORS&lt;/strong&gt;&lt;br&gt;
Since sending a request from the browser to a Last.fm page triggers a CORS error, the request must be made from the backend, which means using an Edge Function from Supabase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Readable Stream&lt;/strong&gt;&lt;br&gt;
In the beginning, I only fetched 20 artists. For me, it wouldn't make sense to invoke 20 Edge Functions for each artist since it would just spend the free tier resources faster. So I tried to make one request to return the data from 20 artists. This is achievable using a Readable Stream.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How it works?
&lt;/h3&gt;

&lt;p&gt;Here's the idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Invoke the Edge Function sending the artists' names array as the request's body&lt;/li&gt;
&lt;li&gt;In the Edge Function, fetch the Last.fm profile page for each artist. Return each HTML page in the response stream&lt;/li&gt;
&lt;li&gt;In the frontend, read the response stream and concatenate its chunks to an accumulator string&lt;/li&gt;
&lt;li&gt;When a chunk is concatenated, check if the accumulator string contains a full artist HTML page. If yes, extract the page from the accumulator string and apply web scraping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How the web scraping works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search for the tags whose content you know that contains country data&lt;/li&gt;
&lt;li&gt;Extract their content as strings&lt;/li&gt;
&lt;li&gt;Search for country names in each string&lt;/li&gt;
&lt;li&gt;The country with more matches is associated with the artist &lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Where do you get the countries' names?
&lt;/h4&gt;

&lt;p&gt;I was already using an amazing map dataset called &lt;a href="https://www.naturalearthdata.com/downloads/" rel="noopener noreferrer"&gt;Natural Earth&lt;/a&gt; to generate Cartogrify's world map. Since it already contains the countries' names, it was an easy choice to use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;String comparison&lt;/li&gt;
&lt;li&gt;Tags can be misleading&lt;/li&gt;
&lt;li&gt;Edge Functions CPU time limit&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  String comparison
&lt;/h4&gt;

&lt;p&gt;Lots of users were complaining that some artists were being associated with strange countries. The ones that attracted more attention were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Michal Jackson was from India.&lt;/li&gt;
&lt;li&gt;Every folk artist was from Norfolk Island.&lt;/li&gt;
&lt;li&gt;Lots of artists were from Saint Barthélemy, Caribbean. Lil Peep, for example.&lt;/li&gt;
&lt;li&gt;Artists from Georgia, USA were from Georgia, a country from Europe/Asia.&lt;/li&gt;
&lt;li&gt;Artists from New Jersey, USA were from Jersey, Channel Islands.&lt;/li&gt;
&lt;li&gt;An American artist named Neon Indian was from... India.&lt;/li&gt;
&lt;li&gt;Gilberto Gil, a fantastic Brazilian artist born in the state of Salvador, was from El Salvador.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's kinda funny, though. Unacceptable but really funny.&lt;/p&gt;

&lt;h5&gt;
  
  
  Why?
&lt;/h5&gt;

&lt;p&gt;There are two approaches I used to compare strings. The exact match and the substring match.&lt;/p&gt;

&lt;p&gt;I started with the exact match because it's the standard logic, right? If it says "Djavan is an artist from Brazil" you split the string by the whitespaces, match "Brazil" with "Brazil" and that's it.&lt;/p&gt;

&lt;p&gt;But it turns out that I was associating lots of artists with plenty of country data with an undefined country. This would happen because "Brazil," or "Brazilian" doesn't match with "Brazil", for example. (Examples are in sentence case but were converted to lowercase before comparison)&lt;/p&gt;

&lt;p&gt;I thought that using the substring match would loosen the match criteria and therefore give better results. Oh, boy. What would happen is that a lot of undesired matches would occur.&lt;/p&gt;

&lt;p&gt;For example, India is a substring of "Michael Jackson is an artist from Indiana". Also, a substring of "Neon Indian". India ended up being the country with the most matches and these artists would be associated with it.&lt;/p&gt;

&lt;h5&gt;
  
  
  Solution
&lt;/h5&gt;

&lt;p&gt;Use the exact match combined with a demonym's exact match. Demonyms, according to Google, is "a noun used to denote the natives or inhabitants of a particular country, state, city, etc". Like Brazilian, American, Italian, etc.&lt;/p&gt;

&lt;p&gt;Lots of times the wiki text would contain something like "Luiz Gonzaga do Nascimento (Exu, Pernambuco, December 13, 1912 — Recife, Pernambuco, August 2, 1989) was a prominent Brazilian folk singer, songwriter, musician and poet."&lt;/p&gt;

&lt;p&gt;There ain't no "Brazil" string but a "Brazilian" one. Without demonyms, Luiz Gonzaga would be associated with an undefined country. With demonyms, he is associated with Brazil. And no substring match problems.&lt;/p&gt;

&lt;p&gt;I got the demonyms list from this &lt;a href="https://en.wikipedia.org/wiki/List_of_adjectival_and_demonymic_forms_for_countries_and_nations" rel="noopener noreferrer"&gt;Wikipedia page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The idea of using something like Levenshtein Distance instead of an exact match also crossed my mind but I just tried to keep it simple.&lt;/p&gt;

&lt;h5&gt;
  
  
  Tags can be misleading
&lt;/h5&gt;

&lt;p&gt;There are only 3 tags you need to search for content in a Last.fm artist page. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadata tag&lt;/li&gt;
&lt;li&gt;Wiki tag&lt;/li&gt;
&lt;li&gt;Tags tag&lt;/li&gt;
&lt;/ul&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%2F2825s2ov8kndtqjdxn8p.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%2F2825s2ov8kndtqjdxn8p.png" width="800" height="604"&gt;&lt;/a&gt;&lt;br&gt;Gilberto Gil profile page in Last.fm&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;The metadata tag is the most valuable because it may contain the exact country data.&lt;/p&gt;

&lt;p&gt;The tags tag may not contain country data at all. But when it contains, it can be a demonym, the country name in its own language or things like that.&lt;/p&gt;

&lt;p&gt;The wiki tag is less valuable because it can contain country data that isn't associated with the country the artist was born. For example, "In the 1970s, Gil added new elements of African and North American music to his already broad palette [...]". As "African" and "American" are respectively demonyms from Africa and the USA, it would count as a match to Africa and the USA.&lt;/p&gt;

&lt;h5&gt;
  
  
  Solution
&lt;/h5&gt;

&lt;p&gt;Instead of counting country matches, use a point approach. Each tag will have a point weight associated with its value. Metadata tags have 5 points, tags tags have 3 points and wiki tags have 1 point.&lt;/p&gt;

&lt;p&gt;When matches occur, the country receives the points according to the tag where the match occurred. The country with the most points wins.&lt;/p&gt;

&lt;h4&gt;
  
  
  Edge Function CPU time limit
&lt;/h4&gt;

&lt;p&gt;The Edge Functions were working fine when I was fetching only 20 artists from Spotify and Last.FM APIs. But when I increased the number, this error started to appear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU time limit reached. isolate: 16597602940236451129
CPU time used: 560ms
hyper::Error(User(Body), hyper::Error(Body, Custom { kind: UnexpectedEof, error: "unexpected EOF during chunk size line" }))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What was annoying me was that this error would sometimes appear and sometimes not. I was aware the number of artists was causing it but I couldn't find why this intermittent behaviour was happening.&lt;/p&gt;

&lt;p&gt;I thought that the root of the problem was the unexpected EOF message but after posting a question in StackOverflow some answers made me realize that it was just the time limit.&lt;/p&gt;

&lt;p&gt;Since I was using a timeout of 1s between each request to avoid being blocked by Last.fm, 30 artists means at least 30s, 35 means 35s and so on.&lt;/p&gt;

&lt;h5&gt;
  
  
  Solution
&lt;/h5&gt;

&lt;p&gt;Migrate the country detection code to AWS Lambda which can run up to 15 minutes. Since an user with 50 unknown artists takes roughly 1 minute, it's ok.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Second Solution
&lt;/h3&gt;

&lt;p&gt;Do you remember the "source pool"? Well, there was an option that didn't appear there. It was MusicBrainz API. &lt;/p&gt;

&lt;p&gt;When I found MusicBrainz API for the first time, I had already decided to follow the Last.fm approach. For that reason, I didn't further explore their API and wasn't aware that there was a resource named Area which holds the artist's location data. That would have solved my problems.&lt;/p&gt;

&lt;p&gt;Fortunately, the same lovely person in the Last.fm Discord gave me this hint about the Area resource. Then, I decided to shift the approach to using MusicBrainz API as its main source. &lt;/p&gt;

&lt;p&gt;That one goes to the "What I learned" section. That was my biggest mistake in the process of finding the solution to this problem. It cost me so much time.&lt;/p&gt;

&lt;h4&gt;
  
  
  How it works?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Invoke the AWS Lambda function passing the artists' names array as the request body&lt;/li&gt;
&lt;li&gt;In the Lambda function, request the MusicBrainz API for the data of each artist.&lt;/li&gt;
&lt;li&gt;Return the data in the response stream.&lt;/li&gt;
&lt;li&gt;Extract the country from the data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the Lambda function code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const https = require('https');
const URL = require('url');

async function getArtistData(artistName) {
  try {
    return new Promise((resolve, reject) =&amp;gt; {
      setTimeout(() =&amp;gt; {
        const req = https.get({
          hostname: 'musicbrainz.org',
          path: `/ws/2/artist/?query=artist:${encodeURIComponent(artistName)}&amp;amp;fmt=json&amp;amp;limit=100`,
          headers: {
            'User-Agent': ###########
          }
        }, (res) =&amp;gt; {
          let body = '';
          res.on('data', (chunk) =&amp;gt; body += chunk);
          res.on('end', () =&amp;gt; resolve(body));
        }); 

        req.on('error', (e) =&amp;gt; reject(e));
        req.end();
    }, 1000);
  });
  } catch (e) {
    return new Promise((resolve, reject) =&amp;gt; reject(e))
  }
}

exports.handler = awslambda.streamifyResponse(async (event, responseStream, _context) =&amp;gt; {
    responseStream.setContentType("text/event-stream");

    const artistsName = event.body.split("###") || [];
    for (const artistName of artistsName) {
      try {
      responseStream.write("START_OF_JSON");
        responseStream.write(JSON.stringify({
          name: artistName,
          data: await getArtistData(artistName)
      }));
          responseStream.write("END_OF_JSON");
      } catch (e) {
        console.log(artistName)
        console.error(e)
        responseStream.write(e)
      }
  }

    responseStream.end();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the code that runs on the Angular app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; findArtistsCountryOfOrigin(artists: Artist[]): Observable&amp;lt;ScrapedArtist&amp;gt; {
    const artists$ = new Subject&amp;lt;ScrapedArtist&amp;gt;();

    const artistsNames = artists.map((artist) =&amp;gt; artist.name);
    fetch(environment.PAGE_FINDER_URL, {
      method: "POST",
      body: artistsNames.join("###"),
    })
      .then(async (response) =&amp;gt; {
        const streamReader = response.body?.getReader();
        if (!streamReader) return;

        const textDecoder = new TextDecoder();
        let streamAccumulatedContent = "";

        while (true) {
          const { value, done } = await streamReader.read();

          streamAccumulatedContent += textDecoder.decode(value);
          if (
            streamAccumulatedContent.includes("START_OF_JSON") &amp;amp;&amp;amp;
            streamAccumulatedContent.includes("END_OF_JSON")
          ) {
            const startIndex =
              streamAccumulatedContent.indexOf("START_OF_JSON") + this.START_INDICATOR_OFFSET;
            const endIndex = streamAccumulatedContent.indexOf("END_OF_JSON");

            const rawArtistData: RawMusicBrainzArtistData = JSON.parse(
              streamAccumulatedContent.slice(startIndex, endIndex)
            );

            streamAccumulatedContent = streamAccumulatedContent.slice(
              endIndex + this.END_INDICATOR_OFFSET
            );

            const artistData = {
              name: rawArtistData.name,
              artistDataFromMusicBrainz: this.musicBrainzService.getArtistData(rawArtistData),
            };

            const { country, secondaryLocation } =
              this.musicBrainzService.getArtistLocation(artistData);

            if (country == undefined &amp;amp;&amp;amp; secondaryLocation != undefined) {
              this.countryService
                .findCountryBySecondaryLocation(secondaryLocation)
                .pipe(
                  switchMap((countryFromSecondaryLocation) =&amp;gt; {
                    if (countryFromSecondaryLocation.NE_ID == -1)
                      return this.lastFmService.getLastFmArtistCountry(artistData.name);
                    return of(countryFromSecondaryLocation);
                  })
                )
                .subscribe({
                  next: (country) =&amp;gt; {
                    artists$.next({
                      name: artistData.name,
                      country: country,
                      secondaryLocation,
                    });
                  },
                  error: () =&amp;gt; {
                    artists$.next({
                      name: artistData.name,
                      country: undefined,
                      secondaryLocation: undefined,
                    });
                  },
                });
            } else if (country == undefined &amp;amp;&amp;amp; secondaryLocation == undefined) {
              this.lastFmService.getLastFmArtistCountry(artistData.name).subscribe({
                next: (country) =&amp;gt; {
                  artists$.next({
                    name: artistData.name,
                    country: country,
                    secondaryLocation,
                  });
                },
                error: () =&amp;gt; {
                  artists$.next({
                    name: artistData.name,
                    country: undefined,
                    secondaryLocation: undefined,
                  });
                },
              });
            } else {
              artists$.next({
                name: artistData.name,
                country,
                secondaryLocation,
              });
            }
          }
          if (done) break;
        }
      })
      .catch((err) =&amp;gt; {
        console.log(err);
      });

    return artists$.asObservable();
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I will refactor it to make it more presentable. I'm in the "make it work" phase.&lt;/p&gt;

&lt;h4&gt;
  
  
  Problems
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Missing country data&lt;/li&gt;
&lt;li&gt;Matching artists' names&lt;/li&gt;
&lt;li&gt;Missing Last.fm underground artists&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Missing country data
&lt;/h5&gt;

&lt;p&gt;Sometimes, MusicBrainz only knows the city or state that an artist comes from. Since we need the country, the full location is necessary.&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%2Fzr80ifsc5jt2mia5pc68.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%2Fzr80ifsc5jt2mia5pc68.png" width="800" height="634"&gt;&lt;/a&gt;&lt;br&gt;Thee Sacred Souls data from MusicBrainz API&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;The solution that I found is to use the &lt;a href="https://geocode.maps.co/" rel="noopener noreferrer"&gt;Free Geocoding API&lt;/a&gt;. You send a request with an address and it returns the full location. Then, it's a matter of finding the country name through string comparison.&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%2Fwtkkarmithq29hxprt1q.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%2Fwtkkarmithq29hxprt1q.png" width="800" height="477"&gt;&lt;/a&gt;&lt;br&gt;Free Geocoding API response to San Diego query&lt;br&gt;

  &lt;/p&gt;

&lt;h5&gt;
  
  
  Matching artists' names
&lt;/h5&gt;

&lt;p&gt;One example that I saw was the Racionais MC's example. &lt;/p&gt;

&lt;p&gt;When you fetch data from an artist in MusicBrainz, it returns a list with the most likely artists to match the artist name you provided sorted by a "likelihood" score.&lt;/p&gt;

&lt;p&gt;At first, I always got the first one because it had a higher chance of being the artist I wanted. But I noticed that that's not always true.&lt;/p&gt;

&lt;p&gt;When searching for Tyler, The Creator, for example, he's not the first on the list. Some artists named only as "The Creator" appear first.&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%2Fmmk35hl8owqsmzix65nu.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%2Fmmk35hl8owqsmzix65nu.png" width="589" height="960"&gt;&lt;/a&gt;&lt;br&gt;MusicBrainz API response to "Tyler, The Creator" query&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;To fix this, I tried to exactly match the artist name for the first 100 artists and it worked. But then I got the Racionais MC's problem.&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%2Fvmvyflrkx4i25x8thvh4.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%2Fvmvyflrkx4i25x8thvh4.png" width="800" height="516"&gt;&lt;/a&gt;&lt;br&gt;Free Geocoding API response to San Diego query&lt;br&gt;

  &lt;/p&gt;

&lt;p&gt;The "Racionais MC's" string I got from Last.fm is actually different from the "Racionais MC's" string I got from MusicBrainz. That's because the ' character has a different encoding so the strings never match.&lt;/p&gt;

&lt;p&gt;In this scenario, I put a fallback that returns the first artist if there's no match in the first 100 artists. That seems to be working for now.&lt;/p&gt;

&lt;h5&gt;
  
  
  Missing Last.fm underground artists
&lt;/h5&gt;

&lt;p&gt;MusicBrainz has data from a lot of artists but the Last.fm underground artists are a special kind of artist whose data only seems to exist in Last.fm.&lt;/p&gt;

&lt;p&gt;When I shifted the approach to use MusicBrainz, I stopped using Last.fm. But due to this missing artist problem, I decided to use Last.fm as a fallback in case MusicBrainz doesn't have the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Solution
&lt;/h2&gt;

&lt;p&gt;Finally, we reached the final solution. As I said, it's the MusicBrainz API solution + Last.fm API as a fallback to fix the missing artists problem.&lt;/p&gt;

&lt;p&gt;The only difference is that I'm no longer doing web scrapping with the Last.fm artist profile page. I received some advice from the same lovely person in the Last.fm Discord to move away from that approach. It's kinda like a "good neighbor" policy.&lt;/p&gt;

&lt;p&gt;What changes is that the data from Last.fm now comes from their API and only the artists' wiki text and tags are available. The techniques to extract content and match countries' names stay the same. This is the approach that I've been using for more than a month now.&lt;/p&gt;

&lt;p&gt;I haven't seen any significant complaints from the users anymore. Actually, there were some compliments that were very nice from users who saw artists being assigned to the wrong countries before.&lt;/p&gt;

&lt;p&gt;Here and there artists are still being assigned to undefined countries and sometimes wrong countries. However, I found that the ones that got undefined usually don't have enough data available to determine their countries.&lt;/p&gt;

&lt;p&gt;The already implemented suggestions feature is doing a great job of reassigning the correct countries for the artists that this solution failed and providing countries for the ones that don't have enough data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from all of this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Share early versions of your project with your users and other developers! Valuable advice may come from some lovely people out there.&lt;/li&gt;
&lt;li&gt;Do not dismiss a development path without exploring it properly.&lt;/li&gt;
&lt;li&gt;Even if you put a lot of effort into improving something, it may fail in cases you couldn't even imagine. Always have a fallback when this happens.&lt;/li&gt;
&lt;li&gt;The user's point of view is always the most important one. It doesn't matter that the artist's country of origin was found if the user closes the tab because it took too long.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it! I hope you learned something or that this will help you with any problem that you are facing. Tell me in the comments what you think about the solutions. Suggestions and feedback are very welcome!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>lambda</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How Cartogrify Was Developed - The Idea</title>
      <dc:creator>Leonardo Holanda</dc:creator>
      <pubDate>Thu, 19 Oct 2023 13:06:00 +0000</pubDate>
      <link>https://dev.to/leoholanda/how-cartogrify-was-developed-the-idea-14a1</link>
      <guid>https://dev.to/leoholanda/how-cartogrify-was-developed-the-idea-14a1</guid>
      <description>&lt;p&gt;Being unemployed sucks. But the only thing that doesn't suck about being unemployed is that you have more time to spend on developing fun projects. The ones whose ideas come to you in the shower, you know?&lt;/p&gt;

&lt;p&gt;In this post, I'm going to talk more about one of them. More specifically, about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How did it all start?&lt;/li&gt;
&lt;li&gt;What is Cartogrify?&lt;/li&gt;
&lt;li&gt;Who developed Cartogrify?&lt;/li&gt;
&lt;li&gt;Stack and hosting providers&lt;/li&gt;
&lt;li&gt;What I learned from all of this&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How did it all start?
&lt;/h2&gt;

&lt;p&gt;In the past 5 years, I have noticed, like most music junkies, the fantastic tools that analyse your music taste through Spotify. &lt;a href="https://www.obscurifymusic.com/" rel="noopener noreferrer"&gt;Obscurify&lt;/a&gt;, &lt;a href="https://pudding.cool/2021/10/judge-my-music/" rel="noopener noreferrer"&gt;How Bad Is Your Streaming Music&lt;/a&gt; and &lt;a href="https://icebergify.com/" rel="noopener noreferrer"&gt;Icebergify&lt;/a&gt; are some great examples.&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%2Fjc9zfrhaq4gfseo67461.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%2Fjc9zfrhaq4gfseo67461.png" width="800" height="317"&gt;&lt;/a&gt;&lt;br&gt;How Bad Is Your Streaming Music landing page
  &lt;/p&gt;

&lt;p&gt;They do an amazing work with the analysis. But something that I appreciated even more was seeing people sharing their results and just talking about the music they love. I have discovered great songs from great people this way, actually.&lt;/p&gt;

&lt;p&gt;One day in 2021, I was thinking: why not develop something like this? It seemed like a funny thing to do. Since it should be something new and different from the ones that already exist, I was trying to find a gap that the other tools haven't covered yet. Then, this idea came to my mind:&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%2F0qxq66phntjv1hbj3yug.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%2F0qxq66phntjv1hbj3yug.png" width="598" height="253"&gt;&lt;/a&gt;&lt;br&gt;Google Keep note I made in August 2021 to not forget about Cartogrify idea
  &lt;/p&gt;

&lt;p&gt;It basically says: get a playlist, find where the artists and the playlist author come from, highlight the artists' countries on a world map and show interesting details about it.&lt;/p&gt;

&lt;p&gt;And that's how Cartogrify was born. 2 years later, after I graduated, concluded my internship and my research, I finally found time to bring it to life.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ok, so... what is Cartogrify?
&lt;/h2&gt;

&lt;p&gt;It's a web app that fetches your top artists from Last.fm and Spotify and finds where they come from.&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%2F3uen71gyza492xklmjdk.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%2F3uen71gyza492xklmjdk.png" width="800" height="366"&gt;&lt;/a&gt;&lt;br&gt;Cartogrify telling you how many different countries your artists come from
  &lt;/p&gt;

&lt;p&gt;Then, it counts how many different countries, regions and continents are associated with your artists. A world map highlighting their countries and a Sankey diagram showing their regions and continents are both generated.&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%2F5lrda2328kw937n6hhob.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%2F5lrda2328kw937n6hhob.png" width="800" height="408"&gt;&lt;/a&gt;&lt;br&gt;Your world map
  &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%2Fty8ehduxenx46e60h7uh.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%2Fty8ehduxenx46e60h7uh.png" width="800" height="389"&gt;&lt;/a&gt;&lt;br&gt;Your sankey diagram
  &lt;/p&gt;

&lt;p&gt;Finally, it compares the geographic data with other users to find how internationally diverse your music taste is.&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%2Finlb77j69i3dl4fyqh9k.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%2Finlb77j69i3dl4fyqh9k.png" width="800" height="408"&gt;&lt;/a&gt;&lt;br&gt;Cartogrify telling you how internationally diverse your music taste is
  &lt;/p&gt;

&lt;p&gt;It also has a ranking showing which country has more diversity and popularity.&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%2Fb4a9fciy8lte7pj07hby.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%2Fb4a9fciy8lte7pj07hby.png" width="800" height="499"&gt;&lt;/a&gt;&lt;br&gt;Cartogrify diversity ranking
  &lt;/p&gt;

&lt;p&gt;You can see it &lt;a href="https://cartogrify.web.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who developed Cartogrify?
&lt;/h2&gt;

&lt;p&gt;Me! I'm Leonardo, a software developer from Brazil. I recently graduated in Information Systems from the Federal University of Alagoas while also being an intern for almost 2 years and working with research for 1 year.&lt;/p&gt;

&lt;p&gt;Here's my &lt;a href="https://www.linkedin.com/in/leonardoulisses/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; and &lt;a href="https://github.com/leo-holanda" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; profiles, in case you want to see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which stack and hosting providers did I use?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Frontend
&lt;/h3&gt;

&lt;p&gt;In the frontend, I used Angular 16 with PrimeNG components and D3.js for data visualization. The deployment is made with Google Firebase Hosting.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Angular?
&lt;/h4&gt;

&lt;p&gt;According to some career advice I received, choosing one technology and focusing on it to increase proficiency can increase the chances of getting a job that uses this technology.&lt;/p&gt;

&lt;p&gt;Because I have already used Angular in my internship for 1 year and 6 months, it seemed reasonable to keep using Angular for that reason since getting a job is a priority for me at this moment.&lt;/p&gt;

&lt;p&gt;Otherwise, I would have probably chosen Svelte to learn something new since I have previously worked with React and Vue.js. Any of them should work just fine, I think.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why PrimeNG?
&lt;/h4&gt;

&lt;p&gt;In my internship, Nebular is used as the component library. Since I have also used Material UI in other projects, why not try PrimeNG and see something new?&lt;/p&gt;

&lt;h4&gt;
  
  
  Why D3.js?
&lt;/h4&gt;

&lt;p&gt;I've already used D3.js in my other project called &lt;a href="https://github.com/leo-holanda/logcharts.io" rel="noopener noreferrer"&gt;LogCharts&lt;/a&gt;. Because of this, I'm familiar with it and know how capable this tool is for data visualization. So it wasn't a difficult choice.&lt;/p&gt;

&lt;p&gt;But the thing they say about D3's learning curve is true. Sometimes, it can be frustrating. I pondered about switching to Chart.js or Google Charts to make only the bar charts in a more straightforward way but decided to be stubborn. Turns out it was a good decision.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Firebase?
&lt;/h4&gt;

&lt;p&gt;Because I have seen lots of people talking about it and using it in their projects. Eventually, I got curious and wanted to try it. The free tier is good for small projects and it turns out to be simple to deploy and manage.&lt;/p&gt;

&lt;p&gt;If things get bigger, I might go to Cloudflare, though.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;PostgreSQL.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why PostgreSQL?
&lt;/h4&gt;

&lt;p&gt;One requirement that was on my mind since the beginning was zero cost. I don't have funds at the moment to use in the project so I'm looking for generous free tiers.&lt;/p&gt;

&lt;p&gt;When I was searching about database hosting providers, I noticed that Supabase not only offers free PostgreSQL database hosting but also a RESTful API that handles CRUD requests.&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%2Feti7ns5l5l4g7mv7y3xd.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%2Feti7ns5l5l4g7mv7y3xd.png" width="365" height="955"&gt;&lt;/a&gt;&lt;br&gt;Supabase pricing page in October 18, 2023
  &lt;/p&gt;

&lt;p&gt;It kills two birds with one stone. Seems like a great deal for me! The unlimited API requests and 500MB database space are nice too.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I think I have to say: I'm not sponsored by Supabase. Oh, how I wish...&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Questions
&lt;/h3&gt;

&lt;p&gt;When I made my choice, I must say that some questions were on my mind. They might seem silly to some people but I decided not to ignore it. Here are they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Is a relational database like PostgreSQL suitable for this project?"&lt;/li&gt;
&lt;li&gt;"Would it be better if I used a non-relational database like MongoDB? What difference does it make?"&lt;/li&gt;
&lt;li&gt;"DynamoDB has 25GB of free storage and 200 million free requests per month. No way my app gets big enough to get out of this free tier! It's future-proof!!"&lt;/li&gt;
&lt;li&gt;"If I choose a hosting provider that doesn't offer a CRUD API, I'm gonna have to build it myself and host it. Is it gonna worth it? What about cost?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, they are divided into three categories: paradigm, scalability and utilities. Let's tackle them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Paradigm
&lt;/h4&gt;

&lt;p&gt;Since I used MongoDB in my internship for 1 year and 6 months, I knew that this relational vs non-relational thing wouldn't make a big difference. My database is not rocket science, after all. Should I care so much about this question?&lt;/p&gt;

&lt;p&gt;Searching about this topic, the main advice seems to be: "If you don't know the answer, use a relational database. It will probably work." So I did this and, until now, it's working.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scalability
&lt;/h4&gt;

&lt;p&gt;DynamoDB seems to be the winner here. It's tempting to just use it and forget about some fears like: "What if Supabase's 500MB is not enough?". But at the same time, Supabase is more straightforward and simple to use.&lt;/p&gt;

&lt;p&gt;Since I didn't know if DynamoDB resources would even be necessary to handle Cartogrify presumably small scale, I just opted for Supabase. Like they say: keep it simple, stupid.&lt;/p&gt;

&lt;p&gt;Until this day (October 18, 2023), with 900 users and 15k artists, I'm using less than 100MB of database space. But, there's a catch: Only Last.fm users can use Cartogrify.&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%2Fbw3jvbygg55mrqxzf8up.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%2Fbw3jvbygg55mrqxzf8up.png" width="800" height="601"&gt;&lt;/a&gt;&lt;br&gt;Project resources usage summary
  &lt;/p&gt;

&lt;p&gt;I have submitted a request to Spotify to use their API's Extended Quota Mode which allows the app to be used by every Spotify user. If they accept my request, more users and artists will come and more space will be used. Will it be enough to justify a migration to DynamoDB then? Let's wait for the next chapters!&lt;/p&gt;

&lt;h4&gt;
  
  
  Utilities
&lt;/h4&gt;

&lt;p&gt;It's not always that a database hosting provider offers you a RESTful API like Supabase does. In this case, you need to develop the API yourself, which isn't a problem since it's just CRUD operations.&lt;/p&gt;

&lt;p&gt;But to use this API, you must send a request to one of its endpoints, which implies that you must set up and deploy a server that listens and responds to these requests. Until now, I haven't found a reason to follow this path instead of using Supabase API.&lt;/p&gt;

&lt;p&gt;If I were using DynamoDB or another database without this utility, there would be a reason: Supabase isn't fit for the project anymore. But I don't know when or even if this day will come so more points to Supabase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;p&gt;In the backend, I used AWS Lambda running JavaScript code in a Node.js environment. Besides that, I also used Supabase Edge Functions running JavaScript code in a Deno environment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Serverless or not?
&lt;/h4&gt;

&lt;p&gt;As you already know, zero cost is a must and a generous free tier is what I'm looking for. Using the &lt;a href="https://free-for.dev/#/" rel="noopener noreferrer"&gt;Free For Developers&lt;/a&gt; website to search for hosting possibilities, I noticed some serverless options that were quite interesting regarding their free tier.&lt;/p&gt;

&lt;p&gt;I didn't know much about this serverless thing so I started searching about it. When I noticed that I didn't needed a server running 24/7 or have to deal with cold starts like the ones from Heroku or Render, the serverless seemed like a great idea.&lt;/p&gt;

&lt;p&gt;Since Supabase already offers free Edge Functions, it just decided to try it and it worked nicely!&lt;/p&gt;

&lt;h4&gt;
  
  
  Why AWS Lambda?
&lt;/h4&gt;

&lt;p&gt;In the beginning, I only used Edge Functions. However, some limitations that I will address in a later post made me migrate the country detection algorithm to another place.&lt;/p&gt;

&lt;p&gt;I searched for alternatives and found that AWS Lambda can run for up to 15 minutes and supports Server Sent Events. That was more than enough to solve my problems. Not only that but a generous free tier. So I decided to try it and it works great too.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Edge Functions?
&lt;/h4&gt;

&lt;p&gt;As I said earlier, it's free, it's from Supabase and it works nicely for any other demand than the country detection code. It just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from all of this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sometimes, it's great to try something new. Sometimes, it's better to seek improvement in what you already know.&lt;/li&gt;
&lt;li&gt;You shouldn't worry so much about getting out of free tiers while developing hobby projects.&lt;/li&gt;
&lt;li&gt;If your app does get out of the free tier due to being viral or something like that, it's actually a good thing. People are using it.&lt;/li&gt;
&lt;li&gt;Sometimes, all you want is just a simple Serverless Function. No machine running 24/7 in the backend and no cold starts.&lt;/li&gt;
&lt;li&gt;To have an experienced mentor to discuss things can surely make a world of difference.&lt;/li&gt;
&lt;li&gt;There's no way you can be good at so many things, like UI/UX, frontend, backend and cloud at the same time. You will just be "enough" in all of them. And that, in my context, is not enough to get a job. So it makes sense to focus on one thing and be "enough" on the others. Contributors are key for big projects!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In the next posts...
&lt;/h2&gt;

&lt;p&gt;In the next posts, I'm gonna talk more about the challenges I faced to make Cartogrify work and also share with you what I learned from them.&lt;/p&gt;

&lt;p&gt;I think I'm gonna start with "How to find an artist's country of origin knowing only his name?". That was an awesome one! But if there are other things you would like to know about the Cartogrify development journey, please let me know!&lt;/p&gt;

&lt;p&gt;See you there! Thanks!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
