Voltar para Blog
NavegadorJavaScriptExtensõesDevToolsEngenharia Reversa

O navegador é uma plataforma de desenvolvimento subestimada

Refiz o LinkedIn por fora e por dentro: layout e um painel de vagas que conversa direto com a API interna deles. Tudo do lado do cliente, para mostrar o quanto dá para desenvolver dentro do próprio navegador.

Diego Dias

Eu desenvolvo em cima de todos sites que acesso

Uma interface entregue no navegador é sugestão, não sentença. O que chega até você é DOM, CSSOM e um monte de listeners, tudo montado na sua máquina, com o seu processador, dentro de um runtime que responde aos seus comandos. Hierarquia visual furada, contraste que reprova em qualquer auditoria de acessibilidade, fluxo que exige seis cliques para uma ação que devia ter um: nada disso é imutável. É só a última decisão que alguém tomou antes do deploy. Em vez de reclamar, eu abro o DevTools e reescrevo.

Comecei a modificar os sites que acesso com userscripts simples, utilizando as extensões Tampermonkey no JavaScript e Stylebot no CSS, e fiquei nessa dupla por anos. As duas resolvem, mas a experiência envelheceu mal e hoje beira o obsoleto: o editor do Tampermonkey é um textarea sem autocomplete, sem inspeção de elementos, sem histórico de versão, o erro só aparece no console, depois de salvo. O Stylebot entrega um seletor e um campo de texto, e boa sorte acertando especificidade contra uma classe hasheada por build. Nenhum dos dois enxerga o outro: CSS e JavaScript do mesmo ajuste vivem em ferramentas separadas, com fluxos e locais de salvamento próprios, sem contexto compartilhado entre eles.

O trabalho real acabava acontecendo fora deles: eu inspecionava no DevTools, montava a regra ali, testava e só depois copiava para a extensão, que virou pouco mais que um lugar para guardar arquivo. Fricção pequena, repetida todo dia, durante anos. Foi daí que nasceu o Njectify, mas isso entra mais adiante.

Isso já virou reputação: quando alguém vê minha tela pela primeira vez, seja em call, seja olhando por cima do ombro, a primeira pergunta quase nunca é sobre o que estou fazendo, é por que seu LinkedIn/GitHub/Gmail está diferente do meu?. A resposta de sempre é a mesma: não está diferente para todo mundo, está diferente porque eu decidi que estaria assim. E isso é uma opção que qualquer navegador dá.


Antes e depois: o mesmo LinkedIn, dois navegadores diferentes

Refiz o grid em três faixas fixas e alinhei a navbar nesse mesmo eixo, corrigindo desalinhamentos horizontais que o layout original carregava. Normalizei o espaçamento vertical dos cards, ajustei alinhamentos que vinham tortos entre ícone, texto e borda, refiz a paleta de cores e o fundo, troquei a tipografia por DM Sans, com pesos de fonte mais consistentes entre título e corpo, e os ícones por Phosphor, transformei o painel de vagas em seções colapsáveis com button acessível por teclado e removi Assine o Premium, Para negócios e o card de anúncio da interface.

A outra frente foi recuperar espaço de coisa que não serve para nada. Jogos de hoje, com Zip, Patches, Mini Sudoku e Tango ocupando um card inteiro da coluna direita, saiu fora. O menu do perfil, que empilhava Itens salvos, Grupos, Newsletters e Eventos em quatro linhas com ícone e rótulo cada, virou uma fileira de quatro ícones: mesma navegação, um quarto da altura. Some isso ao card de anúncio e às chamadas de Premium que já tinham saído, e sobra tela para o que eu realmente abro o LinkedIn para ver.

Uso o LinkedIn há anos e a sensação é de uma interface que muda pouco e devagar. Dá para sentir isso na tela: espaçamentos que não conversam entre si, componentes de gerações diferentes convivendo lado a lado, padrões que envelheceram e que tem muito menos atualizações de UI e UX que uma rede social deveria ter.

LinkedIn original sem nenhuma customização: navbar azul clara padrão do LinkedIn, banner "Consiga emprego por R$ 0" ocupando espaço no topo do feed, cards de post com bordas retas e sombra quase inexistente, "Assine o Premium" visível como item de menu.
LinkedIn depois da customização via Njectify: navbar escura substituindo o azul claro original, painel novo "Com base nas suas preferências" inserido no feed mostrando vagas puxadas em tempo real da API interna do LinkedIn, cards de post redesenhados com sombra pronunciada e cantos mais arredondados, ícones da navbar trocados por Phosphor Icons, rodapé discreto "Customized by Njectify".
AntesDepois
Arraste para comparar: antes e depois.

Modernizar isso do meu lado levou poucas horas. Do lado deles, envolveria um ciclo inteiro de produto para uma base gigantesca de usuários, o que explica a lentidão sem tornar o resultado menos datado.

E não é um print editado. É o LinkedIn real, na minha conta. Fecho a aba, volto amanhã e continua assim, até o dia em que eles resolverem mexer e quebrar todo o meu CSS, porque o Njectify mantém o script e o CSS vinculados ao domínio.

Para todos os efeitos, esse é o LinkedIn que eu uso. E a parte visual, que é a mais fácil de mostrar, é tecnicamente a menos interessante de todas: o painel Com base nas suas preferências é onde a coisa fica boa.

O popup do Njectify aberto no LinkedIn: uma regra ativa para www.linkedin.com, com style.css e script.js rodando na página.
O popup do Njectify aberto no LinkedIn: uma regra ativa para www.linkedin.com, com style.css e script.js rodando na página.

O que dá pra aprender futucando o navegador

O LinkedIn aqui é só um exemplo fácil de mostrar. O ponto é o método: DevTools aberto e curiosidade suficiente para entrar na aba Network e observar o que o próprio site troca com o servidor. Isso já é uma forma de engenharia reversa do comportamento da aplicação, e a janela para fazer isso vem aberta em qualquer navegador.

O LinkedIn tem uma aba /jobs com recomendações. Meu painel faz a mesma coisa dentro do feed, sem trocar de página, e para isso precisa conversar com o backend usando o mesmo contexto da aplicação original.

O primeiro passo é abrir o DevTools, ir em Network, filtrar por graphql e navegar até /jobs. O que apareceu ali foi uma API GraphQL interna, a Voyager API, no endpoint /voyager/api/graphql, com chamadas carregando parâmetros como variables e queryId — tudo visível na própria captura, sem documentação nenhuma por trás. Uma captura já mostra a estrutura da consulta que busca as vagas, incluindo um jobCollectionSlug que nomeia a coleção. Daí em diante foi tentativa e erro na marra: troquei o slug por um palpite, disparei a chamada, olhei o retorno. O sinal de fracasso não é erro — é 200 OK com a lista vazia, que foi o que boa parte dos palpites devolveu. Não tem documentação para consultar, então o critério vira empírico: se voltou vaga, o slug existe; se voltou vazio, ou o nome está errado ou a coleção não vale para a minha conta, e não há como distinguir os dois de fora. As quatro que ficaram no painel (recommended, easy-apply, remote-jobs e top-applicant) são simplesmente as que sobreviveram a esse filtro.

O segundo passo é onde a maioria dessas experiências trava: autenticação. Eu não tenho uma chave de API do LinkedIn, e não preciso de uma. O usuário já está autenticado no navegador, a sessão já existe e as chamadas normais do próprio LinkedIn usam esse contexto. O valor do cookie JSESSIONID é usado como csrf-token nas chamadas autenticadas, e o script reproduz essa requisição dentro da mesma sessão.

Nenhuma senha é capturada e nenhuma autenticação é burlada. O dado acessado é o mesmo que apareceria na aba /jobs. A única diferença é onde ele aparece.

Dito isso, vale ser honesto sobre o que essa camada é. Uma API interna não tem contrato comigo: ela existe para o próprio site, pode mudar de forma ou sumir num deploy qualquer, sem aviso e sem changelog — e o uso dela está sujeito aos termos da plataforma, o que muda bastante a conversa quando o alvo é um sistema de terceiros e não uma ferramenta pessoal. Não é uma integração, é uma dependência frágil que eu aceitei conscientemente porque o custo de quebrar é baixo: se parar de funcionar, eu perco um painel no meu próprio feed.

E a parte mais chata, curiosamente, não foi a API. Foi o DOM.

Nas rotas que inspecionei, existem duas navbars diferentes. A do feed responde a div[data-testid="primary-nav"] e tem cara de React. A de /messaging parece ser Ember mais antigo: não tem data-testid nenhum, e o único jeito de agarrá-la é pelas classes BEM antigas, tipo global-nav__primary-item — aliás, as únicas classes estáveis do site: boa parte do resto são hashes de build, e alguns dos que eu usava já mudaram uma vez no meio do caminho.

Mas o pior foi um bug de só funciona depois do F5. Clicando no menu para ir até /messaging, nada do meu CSS aplicava; recarregando a página, tudo certo. O motivo: navegando por clique, o LinkedIn carrega a rota inteira dentro de um <iframe src="/preload/?_bprMode=vanilla">. O documento principal fica praticamente vazio e a interface real vive lá dentro, enquanto a extensão injeta no documento principal. Como o iframe é same-origin, a saída foi copiar minhas folhas de estilo para dentro dele assim que ele aparece.

O assim que ele aparece é a parte que dá trabalho. Sendo uma SPA, a navegação por clique troca o conteúdo sem recarregar nada, então não existe um evento de load para escutar: o iframe simplesmente surge no DOM em algum momento depois do clique. A saída foi um MutationObserver vigiando a árvore, que dispara a cópia das folhas de estilo no instante em que o elemento entra — mesma técnica, aliás, que resolve boa parte dos casos de meu script não pega depois de navegar em qualquer aplicação desse tipo.

Esse tipo de problema é parte da graça. Quando você começa a tratar uma aplicação de terceiros como algo observável e extensível, descobre que cada produto tem suas próprias peculiaridades arquiteturais escondidas atrás da interface.


O teto disso é bem mais alto do que parece

Até aqui foi relato do que eu fiz. O que vem agora é o contrário: cenários que a mesma técnica alcança, mas que eu não construí no LinkedIn. Trato como hipótese, não como prova.

O que fiz lá é simples perto do que dá para fazer com a mesma ideia. Interceptar uma chamada de rede não serve só para observar o que ela retorna: dependendo do ponto em que você intercepta, dá para criar uma camada intermediária no cliente e mudar o comportamento que a aplicação enxerga.

Na prática, interceptar fetch e XMLHttpRequest pode funcionar como um proxy local. Uma chamada para um endpoint específico pode ser redirecionada, mockada, cacheada ou transformada antes de chegar à camada que consome aquele dado, e a aplicação passa a renderizar o que você devolveu, não o que o servidor mandou.

O exemplo mais banal disso é um painel que devolve 500 registros e insiste em paginar de 20 em 20. Você embrulha o fetch, deixa a chamada sair normalmente, e no caminho de volta concatena as páginas antes de entregar o JSON para a aplicação. Ela não sabe que foi interceptada: recebe um array maior do que o servidor mandou naquela requisição e renderiza a tabela inteira de uma vez.

Aquele painel que mostra tudo na tela mas não exporta nada talvez já tenha um endpoint devolvendo o JSON completo; falta só o botão que transforma aquilo em CSV, e o botão pode ser seu.

Um sistema legado pode expor uma resposta quase útil, mas incompatível com o formato que você precisa. Em vez de levantar backend, banco, fila e infraestrutura para resolver uma transformação pessoal ou interna, dá para criar um adapter no cliente que reorganiza aquele payload dentro da própria sessão.

Com WebSocket, a mesma lógica se estende para tempo real. Um sistema de monitoramento que já recebe eventos por socket para atualizar a tela pode ter esse mesmo fluxo escutado por um handler seu: os eventos continuam indo para a aplicação, e em paralelo você acumula, filtra e monta um painel com o recorte que interessa. Não é dado novo, é dado que já passava por ali sem ninguém aproveitar, e não depende do fornecedor abrir uma API para isso.

Nada disso substitui uma integração oficial, e nem deveria. Mas existe uma categoria enorme de problema interno, pessoal ou de produtividade onde levantar servidor, provisionar infraestrutura e manter uma integração inteira é exagero para o tamanho da dor. Às vezes o código pode viver exatamente onde o problema acontece.

O ganho financeiro aparece aí. Pelo caminho convencional, boa parte dessas soluções vira um projeto de integração, um webhook hospedado em algum lugar ou uma feature presa a um plano mais caro. No navegador, dependendo do caso, é código rodando na sua própria sessão: sem servidor no meio, sem infraestrutura e sem esperar alguém aprovar.


O DevTools é seu melhor amigo

Todo o trabalho no LinkedIn saiu de três ou quatro recursos que estão no DevTools há anos, e que quase ninguém usa como ferramenta de desenvolvimento sobre aplicação alheia.

O Network foi por onde a Voyager API apareceu. Com Overrides, o mesmo painel deixa de só observar e passa a substituir: você mapeia arquivos locais sobre os recursos de um site inteiro e recarrega a página rodando o seu código no lugar do dele. Snippets guarda scripts JavaScript persistentes para executar em qualquer aba, que é onde os palpites de jobCollectionSlug foram parar antes de virar extensão.

Quando não dá para ler o código, porque ele veio embaralhado para ocupar menos espaço, o navegador desembaralha: publicado o source map, o DevTools mostra o código como o programador escreveu, com os nomes originais de volta. E quando você não sabe nem onde olhar, o caminho é inverter a pergunta — monitorEvents(elemento) faz o console anunciar tudo o que acontece com um pedaço da tela, você clica e vê o que a página dispara. Foi assim que achei mais de um handler em código que não escrevi.

O resto do painel segue a mesma lógica e cabe numa linha: Performance e Memory para descobrir por que a aba está lenta, Coverage para ver quanto do que foi baixado nunca rodou, IndexedDB para dado estruturado que sobrevive ao fechar da aba.

Daria para empacotar isso num curso de Hotmart, com nome empolgado e promessa de destravar o navegador. Provavelmente já existe, mais de um. E o material todo está na documentação oficial do Chrome, de graça, a um F12 de distância. Não são truques secretos: são peças de uma plataforma que normalmente usamos apenas para abrir sites.


De onde o Njectify saiu

O Njectify nasceu da necessidade que mencionei no começo do post: uma extensão para Chrome em Manifest V3 que junta CSS e JavaScript no mesmo fluxo, com inspetor visual, edição CSS point-and-click, IDE com Monaco Editor, o mesmo do VS Code, console de debug próprio, cofre de segredos e sincronização opcional via Google Drive.

No fundo é uma extensão de Chrome para injetar CSS e JavaScript em qualquer site, e o funcionamento é mais simples do que parece. Você escreve um CSS e um JavaScript e diz a que endereço eles pertencem, no caso www.linkedin.com. A extensão guarda esses arquivos na sua máquina e fica de olho na barra de endereço: toda vez que você abre aquele site, ela injeta o seu código na página logo depois que o site carrega o dele. O seu vem por último, então é o seu que vale.

É por isso que a customização não some. Nada disso foi enviado para o LinkedIn, ninguém do outro lado sabe que existe, e o servidor deles continua mandando a mesma página para todo mundo. A diferença acontece entre a resposta chegar e a tela ser desenhada, e como esse passo se repete a cada carregamento, o resultado parece permanente. Fecho o navegador, volto na semana seguinte, e está lá. Só quebra no dia em que o LinkedIn mudar o próprio código a ponto de o meu não encontrar mais onde se encaixar.

O redesign do LinkedIn não é um produto à parte: foi feito inteiro com a própria extensão.


O navegador nunca foi só um leitor de sites

Vale olhar o tamanho real do que esse experimento exigiu, porque a lista é maior do que parece. Teve leitura de DOM para achar os pontos de ancoragem, CSS resolvendo especificidade contra classes que mudam a cada build, grid e tipografia refeitos, JavaScript rodando dentro da página, observação de tráfego de rede para entender um contrato de API que ninguém documentou, GraphQL com parâmetros descobertos na tentativa e erro, reaproveitamento do contexto de autenticação que já existia na sessão, manipulação de iframe same-origin para contornar uma rota que carregava por dentro, armazenamento local para o ajuste sobreviver ao fechar da aba, e um ciclo de depuração inteiro para descobrir por que nada aplicava antes do F5.

É trabalho de front-end, de engenharia reversa e de integração ao mesmo tempo. E nada disso precisou de servidor, de credencial especial, de permissão de ninguém ou de uma linha de código que não fosse minha rodando na minha máquina. O que muda não são as peças, que já estavam todas ali: é a forma de enxergá-las.

Existe uma situação curiosa na arquitetura da web: para usar uma aplicação moderna, você precisa receber uma parte dela. Interface, código, estrutura, recursos, estado e chamadas de rede chegam até a sua máquina porque o navegador precisa executar aquele produto.

A maioria das pessoas vê isso apenas como implementação. Eu comecei a enxergar como superfície de desenvolvimento, e é daí que vem a diferença entre abrir um site e programar em cima dele.

De trocar a cor de um botão em 2012 até reescrever boa parte do LinkedIn hoje existe uma linha contínua. O código chega até a sua máquina para ser executado, e desde que a web existe, olhar esse código sempre levou alguém a querer mexer nele.

O Njectify nasceu dessa obsessão. Não porque faltasse uma API mágica ou uma vulnerabilidade escondida, mas porque as peças para modificar a experiência já estavam ali. Faltava organizá-las numa ferramenta.



Comentários

Seja o primeiro a comentar

0/1000