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

.NET

O Runlight roda no .NET 10 ou mais recente, dentro de um app ASP.NET Core ou em um domínio próprio como servidor independente.

O pacote .NET é o Runlight reescrito em C#. 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 .NET 10 ou mais recente e do driver ADO.NET do seu banco de dados, que seu app já tem se usa esse banco.

Instalação

Adicione o pacote do ASP.NET Core e o driver do seu banco de dados. O pacote já traz junto o pacote principal Runlight.

bash

dotnet add package Runlight.AspNetCore
dotnet add package Microsoft.Data.Sqlite

O pacote principal não tem dependências próprias. Ele acessa o banco de dados pelo driver que você entrega a ele e compartilha o pool de conexões desse driver.

Crie a instância

Crie uma instância quando o app iniciar e registre-a como singleton.

Program.cs

using Microsoft.Data.Sqlite;
using Runlight;
using Runlight.Store;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(new Runlight.Runlight(new RunlightOptions
{
    Store = Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db"),
    Site = new SiteOptions { Name = "example.com", Hostnames = ["example.com"], Timezone = "Europe/London" },
}));

As opções têm os nomes de Configuração, escritos em PascalCase, então Sites, TrustProxy, RateLimit, Mail e ManagedSites funcionam como lá. As tabelas são criadas na primeira requisição.

Stores

As tabelas do Runlight começam com rl_. Todo store aceita um argumento prefix para usar outro prefixo no lugar, como Stores.Sqlite(SqliteFactory.Instance, path, prefix: "wp_rl_"), 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. store.Tables() lista as tabelas do store pelo nome, para um app que está removendo o Runlight.

SQLite

csharp

Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db");

O SQLite precisa do Microsoft.Data.Sqlite. Guarde o arquivo em uma pasta onde o app possa escrever, já que o SQLite escreve um journal ao lado dele. A pasta é criada se não existir.

Postgres

csharp

Stores.Postgres(NpgsqlDataSource.Create("Host=127.0.0.1;Username=runlight;Password=password;Database=runlight"));

Este store precisa do Npgsql. Passe o próprio NpgsqlDataSource do app para compartilhar o pool dele. Um segundo argumento como 30000 interrompe qualquer consulta depois desse número de milissegundos, e Search Path=analytics na connection string mantém as tabelas em um schema próprio.

MySQL e MariaDB

csharp

Stores.MySql(new MySqlDataSource("Server=127.0.0.1;User ID=runlight;Password=password;Database=runlight"));

Este store precisa do MySqlConnector e funciona com MySQL 8.0.20 ou mais recente e MariaDB 11.4 ou mais recente. Ele aceita o mesmo segundo argumento do store do Postgres.

ASP.NET Core

Mapeie o Runlight depois que o app for construído.

Program.cs

var app = builder.Build();

app.MapRunlight();

app.Run();

MapRunlight() serve o painel, a API e o script em /runlight, e os links curtos em /go. Ele também responde em qualquer domínio de links que você adicionar em Configurações, e registra as leituras de agentes de IA enquanto suas páginas são servidas, sem atrasar ninguém mais. O Runlight verifica as próprias requisições, então os endpoints dele pulam a verificação antiforgery do ASP.NET Core, que recusaria o script de rastreamento. Eles aceitam requisições anônimas, então um app cuja política de fallback exige um usuário logado continua contando os visitantes. 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. As opções das rotas ficam em um RoutesOptions, com os nomes de Configuração.

csharp

app.MapRunlight(new RoutesOptions { BasePath = "/stats", Accounts = true, Origin = "https://example.com" });

Com Accounts = true e um RUNLIGHT_SECRET, as pessoas entram com o próprio e-mail e senha, como em Contas. Com Token = null as rotas não têm login próprio, e os endpoints passam a seguir a política de autorização do app.

app.UseRunlight() faz o mesmo trabalho como middleware, para um app que quer o Runlight antes dos outros middlewares ou que não usa endpoint routing. As requisições que não são do Runlight seguem para o app. O middleware não tem autorização de endpoint, então chame UseAuthentication() e UseAuthorization() antes dele quando o painel depender do login do app.

MapRunlight() também funciona dentro de UseEndpoints, onde responde aos domínios de links logo antes dos seus endpoints. Num grupo de rotas de MapGroup ele se recusa a iniciar, já que as rotas precisam do caminho completo, então mapeie-o no app e coloque em BasePath o caminho completo, como /admin/runlight. Um link curto que ninguém tem fica para o seu app. MapRunlight() responde a ele com um 404 sem corpo, como o ASP.NET Core responde a qualquer caminho sem endpoint, então UseStatusCodePages mostra ali a sua própria página, e UseRunlight() passa a requisição para o app.

Um domínio de links é um host próprio, então adicione-o a AllowedHosts quando o app definir essa lista, ou o ASP.NET Core recusa a requisição antes.

Outros apps .NET

Qualquer app .NET pode entregar uma requisição ao Runlight e devolver a resposta dele. Monte um Runlight.Http.Request a partir da URL absoluta, do método, dos cabeçalhos, do corpo e do endereço da conexão, e passe-o para FrontController.AnswerAsync().

csharp

using Runlight.Http;
using Runlight.Server;

var request = new Request(url, method, headers, body, remoteAddress);
Response answer = await FrontController.AnswerAsync(rl, routes, request);
await rl.IdleAsync();

answer.Status, answer.HeaderLines() e answer.WriteToAsync() dão o que enviar. IdleAsync() executa o trabalho que a biblioteca TypeScript faz depois de responder, então chame-o assim que a resposta tiver saído. Em um endpoint ou middleware ASP.NET Core seu, RunlightHttp.ServeAsync(context, rl, routes) faz tudo isso para um HttpContext.

A verificação agendada

A verificação agendada pode rodar dentro do app como um hosted service.

RunlightCheck.cs

public sealed class RunlightCheck(Runlight.Runlight rl) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
        do
        {
            await rl.CheckAsync(stoppingToken);
            await rl.IdleAsync();
        }
        while (await timer.WaitForNextTickAsync(stoppingToken));
    }
}

Registre-o com builder.Services.AddHostedService<RunlightCheck>(). Um app que roda em várias cópias pode deixar isso de fora e definir CRON_SECRET, e então fazer um único agendador chamar /runlight/api/check.

O servidor independente

A ferramenta runlight roda o Runlight em um domínio próprio, como stats.example.com, do mesmo jeito que o servidor independente. Ela serve o painel na raiz, mantém os sites no painel e tem contas com login e links curtos em qualquer domínio que você apontar para ela.

bash

dotnet tool install --global Runlight.Server
mkdir stats && cd stats
runlight serve

O servidor escuta na porta 3000, ou em PORT, no Kestrel. Os dados ficam em runlight-data na pasta de onde você o inicia. Na primeira inicialização ele mostra um link com um código de uso único que cria a primeira conta e grava o link em setup.txt na pasta de dados, e runlight setup mostra o link de novo. O link só funciona enquanto não existe nenhuma conta. Funções, convites, login em duas etapas e adição de sites funcionam como no servidor independente.

Configurações

As configurações vêm de variáveis de ambiente ou de um config.json na pasta de onde você inicia o servidor, com os mesmos nomes. As variáveis de ambiente prevalecem quando as duas estão definidas.

config.json

{
  "RUNLIGHT_URL": "https://stats.example.com",
  "DATABASE_URL": "postgres://runlight:password@localhost:5432/runlight"
}
Configuração O que faz
PORT A porta em que o servidor escuta, 3000 por padrão.
HOST O endereço em que ele escuta, 0.0.0.0 por padrão.
RUNLIGHT_URL O endereço público do painel, como https://stats.example.com. Ele nunca pode virar um domínio de links, e os e-mails de convite e de relatório apontam para ele.
DATABASE_URL Um endereço postgres://, mysql:// ou mariadb:// guarda os dados nesse banco de dados em vez do SQLite.
DATA_DIR A pasta de dados, runlight-data por padrão. Um caminho relativo é lido a partir da pasta onde o servidor inicia.
RUNLIGHT_SECRET A chave que assina os logins e criptografa as chaves salvas. Sem ela, o servidor cria uma e a guarda na pasta de dados.
RUNLIGHT_TOKEN Um bearer token para scripts, que também cria a primeira conta no lugar do link de configuração.
TRUST_PROXY Defina como false quando não houver nada na frente do servidor, ou como x-real-ip ou cf-connecting-ip quando esse cabeçalho trouxer o endereço do visitante.
RUNLIGHT_GEO De onde vêm as localizações, como no servidor independente. É city por padrão, e pode ser country, off ou o caminho de um arquivo MMDB.
CRON_SECRET Permite que um agendador rode a verificação em POST /api/check.
RUNLIGHT_OBSERVE_KEY Uma única chave com que qualquer site pode informar agentes de IA. A chave de agentes de cada site, mostrada em Configurações, Instalação, é a melhor escolha.
RUNLIGHT_CONFIG O caminho de um arquivo de configuração fora da pasta onde o servidor inicia.

O servidor roda a verificação agendada a cada cinco minutos e baixa os novos dados de localização uma vez por mês. Para um servidor que não fica sempre no ar, runlight cron faz o mesmo trabalho uma vez. Ele não mostra nada quando está tudo bem, a não ser uma linha quando os dados de localização de um novo mês ficam prontos.

bash

*/5 * * * * cd /srv/stats && runlight cron

Um serviço do systemd ou qualquer gerenciador de processos mantém o servidor rodando. Ele para com SIGTERM e dá cinco segundos para as respostas que já começou terminarem.

Senhas esquecidas

Rode o comando password na pasta do servidor para dar uma nova senha a uma conta, que ele mostra na tela. Ele também desativa o login em duas etapas dessa conta, e cria a conta quando ela não existe.

bash

runlight password someone@example.com

Atualizações e backups

Rode dotnet tool update --global Runlight.Server e reinicie o servidor. As tabelas se atualizam sozinhas na próxima inicialização, ou rode runlight migrate para atualizá-las na hora. Faça backup da pasta de dados e do config.json, já que as chaves salvas não podem ser lidas sem o segredo.

Agentes de IA a partir de um log

Agentes de IA como GPTBot e ClaudeBot leem páginas sem rodar JavaScript, então a tag do script nunca os vê. Um app ASP.NET Core com o Runlight dentro os conta enquanto serve as páginas. Para qualquer outro site que você hospeda, o log de acesso do servidor web as registra, e o comando agents lê esse log.

bash

runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --follow

A chave é a chave de agentes do site, mostrada no Runlight em Configurações, Instalação, e ela só pode informar leituras desse site. Quando --to ou --key ficam de fora, o comando lê RUNLIGHT_URL ou RUNLIGHT_OBSERVE_KEY do ambiente ou do config.json. --site é o endereço do site, para logs no formato padrão do nginx ou do Apache, que deixam o host de fora. Os logs JSON do Caddy trazem o host, então ali ele não é necessário.

Com --follow o comando continua rodando, envia as leituras conforme acontecem e segue em frente quando o log é rotacionado. Adicione --state agents.json para que um reinício continue de onde parou. Sem --follow ele lê o log uma vez e para, e com --state a próxima execução começa onde a última terminou, o que combina com cron. Só uma execução por vez pode usar um arquivo de estado, e uma trava deixada por uma execução que caiu é assumida pela próxima. Só as leituras de página bem-sucedidas de agentes de IA conhecidos saem da máquina, cada uma com o endereço da página, o user agent do agente e quando foi servida.

Diferenças em relação à biblioteca TypeScript

O pacote .NET 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 os apps .NET rodam.

  • O pool de conexões é o do driver. Postgres e MySQL pegam uma conexão do pool da data source para cada consulta, e o SQLite mantém uma conexão aberta e executa uma instrução por vez.
  • O que a biblioteca TypeScript faz em segundo plano, como apagar visitas depois que uma retenção menor é escolhida, roda assim que o ASP.NET Core envia a resposta.
  • As senhas recebem hash com scrypt escrito em C#, já que o .NET não tem um embutido. Os hashes batem com os da biblioteca TypeScript, então as contas continuam valendo.
  • O limite de requisições do script de rastreamento fica na memória, então cada cópia do app conta por conta própria. Os limites de senha errada ficam no banco de dados, então todas as cópias os compartilham.
  • Os relatórios por email formatam números, datas, valores e nomes de países a partir de uma cópia dos dados ICU que o Node usa, então ficam iguais em qualquer sistema.