Todas as docs

Início

  1. 01Primeiros passos
  2. 02Instalação
  3. 03Rastreamento

Recursos

  1. 04O painel
  2. 05Metas
  3. 06Links curtos
  4. 07Origens e agentes de IA
  5. 08Relatórios por email
  6. 09Verificação agendada
  7. 10Pergunte à sua IA

Referência

  1. 11Configuração
  2. 12API HTTP

Plataformas

  1. 13Servidor independente
  2. 14PHP
  3. 15WordPress
  4. 16Drupal
  5. 17Craft CMS
  6. 18Python
  7. 19Rails
  8. 20Ruby
  9. 21Go
  10. 22Java
  11. 23.NET
  12. 24Elixir
  13. 25Rust

Confiança

  1. 26Privacidade

Plataformas

Elixir

O Runlight roda no Elixir 1.18 ou mais recente, sobre Erlang/OTP 27 ou mais recente, dentro de um app Phoenix ou Plug, com as tabelas no seu próprio repo Ecto.

O pacote Elixir é o Runlight reescrito em Elixir. Ele serve o mesmo painel e a mesma API, e dá a cada requisição a mesma resposta que a biblioteca TypeScript dá. Os números ficam nas mesmas tabelas, então qualquer um dos dois consegue ler um banco de dados que o outro escreveu. Ele precisa do Elixir 1.18 ou mais recente, sobre Erlang/OTP 27 ou mais recente.

Instalação

Adicione o pacote ao mix.exs, junto com o adapter do Ecto para o seu banco de dados, se o seu app ainda não tiver um.

mix.exs

def deps do
  [
    {:runlight, "~> 0.1"},
    {:ecto_sqlite3, "~> 0.17"}
  ]
end

O Runlight guarda as tabelas no próprio repo Ecto do seu app, por meio de ecto_sqlite3, postgrex ou myxql. O painel dele é um Plug, que todo app Phoenix já tem. Os fusos horários vêm do pacote tz, que é compilado junto, então uma release funciona em qualquer imagem.

Inicie a instância

O Runlight é um filho da árvore de supervisão da sua aplicação, iniciado depois do seu repo.

lib/my_app/application.ex

children = [
  MyApp.Repo,
  {Runlight,
   store: {Runlight.Store, repo: MyApp.Repo},
   site: [name: "example.com", hostnames: ["example.com"], timezone: "Europe/London"],
   check_every: :timer.hours(1)}
]

As opções têm os mesmos nomes de Configuração, escritos em snake case, então sites, trust_proxy, rate_limit, mail e managed_sites funcionam como lá. As tabelas são criadas quando a instância inicia, e um store que não pode ser alcançado impede o app de subir. check_every roda a verificação agendada em um intervalo, o que troca os salts diários, envia os relatórios por email que estão na hora, aplica por quanto tempo cada site guarda as visitas e soma os totais diários que deixam os períodos longos rápidos. Deixe-o de fora quando um agendador chamar /runlight/api/check com CRON_SECRET, como mostra Verificação agendada, ou chame Runlight.check(Runlight.instance()) a partir de um job seu.

Stores

O store é o seu repo, e o adapter dele define o SQL que o Runlight escreve. Todas as tabelas do Runlight começam com rl_, então ficam ao lado das suas. prefix: "wp_rl_", como em {Runlight.Store, repo: MyApp.Repo, prefix: "wp_rl_"}, usa outro prefixo no lugar, para que dois apps que dividem um banco de dados mantenham cada um suas próprias tabelas. Um prefixo tem até 32 letras, dígitos e sublinhados, e não começa com um dígito, e Runlight.Store.tables(store) lista as tabelas do store pelo nome, para um app que está removendo o Runlight.

No SQLite, mantenha o journal write-ahead do ecto_sqlite3 e dê ao repo um busy_timeout de pelo menos 5000. Um banco de dados em memória existe um por conexão, então um repo com database: ":memory:" precisa de pool_size: 1.

O Postgres funciona como está. O Runlight cria os índices de forma concorrente, então uma tabela grande continua recebendo escritas enquanto uma atualização roda.

O MySQL precisa da versão 8.0.20 ou mais recente, e o MariaDB da 11.4 ou mais recente, já que as tabelas usam a collation binária utf8mb4_0900_bin para que o texto seja ordenado do mesmo jeito em todo banco de dados.

Phoenix

Encaminhe /runlight para o Plug fora do pipeline :browser. Esse pipeline pede um token CSRF em todo POST, que o script de rastreamento não consegue enviar, e o Runlight verifica as requisições do seu próprio jeito.

lib/my_app_web/router.ex

scope "/" do
  forward "/runlight", Runlight.Plug, token: System.get_env("RUNLIGHT_TOKEN", "")
  forward "/go", Runlight.Plug.Links
end

O token é lido uma vez, quando o router compila. Em uma release, deixe a opção de fora e defina RUNLIGHT_TOKEN onde o app roda, já que o Runlight o lê na primeira requisição. Um token vazio mantém o painel fechado, e token: false o deixa aberto para um app que verifica as pessoas por conta própria.

Adicione o script a todas as páginas, logo antes de </head>.

html

<script defer src="/runlight/s.js"></script>

Depois defina RUNLIGHT_TOKEN como uma string aleatória longa e abra uma vez /runlight/?token= seguido dessa string para entrar, como descreve Primeiros passos. Com accounts: true e um RUNLIGHT_SECRET, as pessoas entram com o próprio e-mail e senha, como em Contas.

Para contar os agentes de IA que leem suas páginas, adicione o observer ao pipeline :browser. Ele registra as leituras deles e retorna na hora para todos os outros.

lib/my_app_web/router.ex

pipeline :browser do
  plug :accepts, ["html"]
  plug Runlight.Plug.Observer
end

Um domínio de links responde a links curtos em um nome próprio. Cada Plug do Runlight responde a uma requisição em um domínio de links antes de qualquer outra coisa e repassa todas as outras requisições sem mexer nelas. Um router do Phoenix só os executa para os caminhos que ele reconhece, então coloque o Plug de domínio de links no seu endpoint, antes do router, e toda requisição nesse nome é respondida ali.

lib/my_app_web/endpoint.ex

plug Runlight.Plug.LinkDomains
plug MyAppWeb.Router

Um nome que nenhum link tem fica para o seu app responder. Encaminhado de um router do Phoenix, Runlight.Plug.Links levanta o NoRouteError do próprio Phoenix, então a sua página 404 aparece, e encaminhado de um Plug.Router simples ele responde 404. Você também pode colocar plug Runlight.Plug.Links no seu endpoint antes do router, onde ele substitui tanto o forward quanto o Plug de domínio de links. Ali ele responde aos domínios de links e a /go/<name> para os links que existem, e repassa todas as outras requisições para o seu app, inclusive um nome desconhecido. Num domínio de links, /go/<name> é sempre um 404, já que os links de um domínio de links ficam na raiz do nome dele.

Plug.Router

Um Plug.Router encaminha do mesmo jeito. O Plug de domínio de links vem antes de :match, para ver todas as requisições.

lib/my_app/router.ex

defmodule MyApp.Router do
  use Plug.Router

  plug Runlight.Plug.LinkDomains
  plug :match
  plug :dispatch

  forward "/runlight", to: Runlight.Plug
  forward "/go", to: Runlight.Plug.Links
end

Atrás de um proxy

O Runlight lê o endereço do visitante na última entrada de X-Forwarded-For, que o proxy mais próximo escreveu, para que um visitante conte uma vez por dia. Sem nada na frente do app, defina trust_proxy: false e ele lê o endereço da própria conexão. Atrás da Cloudflare e de outro proxy, trust_proxy: "cf-connecting-ip" lê só esse cabeçalho.

Várias instâncias

Dê um nome a cada uma e passe-o para os Plugs.

elixir

{Runlight, name: MyApp.Stats, store: {Runlight.Store, repo: MyApp.Repo}, site: [name: "Docs"]}

forward "/stats", Runlight.Plug, instance: MyApp.Stats

Diferenças em relação à biblioteca TypeScript

O pacote Elixir passa nos testes de conformidade aos quais a biblioteca TypeScript é submetida, no SQLite, Postgres, MySQL e MariaDB. As diferenças vêm do jeito como o Elixir roda.

  • Cada requisição roda em um processo próprio, e a instância vive na sua árvore de supervisão. O estado dela, como o limite de requisições do script de rastreamento e as listas de domínios de links em cache, fica em uma tabela ETS, então todos os processos de um nó o compartilham e cada nó mantém o seu.
  • O que a biblioteca TypeScript faz em segundo plano, como apagar visitas depois que uma retenção menor é escolhida, roda em uma task própria assim que a resposta é enviada.
  • As senhas novas recebem hash com PBKDF2-SHA-256, que o crypto do Erlang calcula nativamente. Os hashes que a biblioteca TypeScript fez com scrypt continuam permitindo o login, então as contas continuam valendo.
  • Não existe servidor independente em Elixir. Para um único painel com vários sites, rode o servidor Node ou o servidor de outra linguagem, e conecte o site deste app a ele.