Runlight (runlight v0.1.0)

Copy Markdown View Source

Privacy friendly web analytics that lives inside your Elixir app: the same library as @runlight/sdk, the same tables, and the same answers, so an Elixir app and a Node or PHP one can share a database.

An instance is a child of your application's supervision tree:

children = [
  MyApp.Repo,
  {Runlight,
   store: {Runlight.Store, repo: MyApp.Repo},
   site: [name: "Example", hostnames: ["example.com"], timezone: "Europe/London"]}
]

and the dashboard, the API, and the tracker are a Plug, mounted in a Phoenix router or a Plug.Router (see Runlight.Plug):

forward "/runlight", Runlight.Plug

Then add <script defer src="/runlight/s.js"></script> to your pages.

Options

The options are the SDK's, in snake case:

  • :store - {Runlight.Store, repo: MyApp.Repo}, or a Runlight.Store.
  • :site - the site this install counts: id (default "default"), name, hostnames (without www; empty means any host), and timezone (an IANA name, default "UTC").
  • :sites - several sites, told apart by hostname; each needs its hostnames.
  • :managed_sites - sites are added, changed, and deleted in the dashboard and kept in the database, as the standalone server does.
  • :geo - a lookup for an IP when the platform sends no location headers, such as Runlight.Geo.file_lookup("GeoLite2-City.mmdb").
  • :trust_proxy - read the client's address from forwarding headers: true (the default: the last X-Forwarded-For entry, then X-Real-IP, then CF-Connecting-IP), one header's name, or false for the connection's own address. Left unset, it warns once when a request comes straight from a public address with none of those headers.
  • :link_path - where short links on the app's own domain live, default "/go".
  • :mail - the mail service for email reports, set in code.
  • :secret - encrypts the keys kept in the database; default RUNLIGHT_SECRET, then RUNLIGHT_TOKEN.
  • :rate_limit - tracker requests per address per minute, default 120; false for none.
  • :fetcher - every outgoing request goes through it (see Runlight.Fetch); default &Runlight.Fetch.httpc/2.
  • :local_installs - lets a connected install be at http://localhost or http://127.0.0.1, for trying a hub and an app on one machine. Default false: otherwise anyone who can add a site could have this server ask services on its own machine, so other installs must be public https addresses.
  • :now - a function answering epoch milliseconds, for tests.
  • :name - the instance's name, default Runlight.

Summary

Functions

Adds a site, when sites are managed in the dashboard: one counted here, or one connected from another install.

The dashboard assistant's provider, model, and key, kept sealed like the mail keys. Nil until an owner sets it up.

Adds up each site's finished days, so long ranges read a row a day instead of every visit. A day is built two hours after it ends in the site's timezone, and at most ten days a run.

Scheduled upkeep, safe to run every minute. It rotates salts, sends the email reports that are due, deletes visits past each site's retention, and builds daily rollups. It also rereads sites, their dashboard settings, and connected installs, so a change made by another process shows up here too.

The visitor's address, for the daily visitor hash and the rate limit. Behind a proxy it comes from a header: by default the last X-Forwarded-For entry, which the nearest proxy wrote, then X-Real-IP and CF-Connecting-IP.

Handles one tracker request. Always succeeds; bad input is dropped quietly.

Deletes a site and everything recorded for it, when sites are managed in the dashboard.

Builds and sends one report. Also used by "Send a sample now".

What passes for an email address: something@somewhere.tld, with no spaces, quotes, or angle brackets.

An environment variable, trimmed, or nil when it is empty.

An outgoing request through the instance's fetcher (see Runlight.Fetch).

Clears the cached link domains after one is added or removed.

Forgets what a connected install said, after a change made through it.

Retention work still running; the scheduled check and tests wait for it.

Creates tables and records the configured sites. Runs once.

The instance started under name in a supervision tree.

A path on every link domain that answers when the domain reaches this Runlight.

For middleware: when a request arrives on a link domain added in Settings, answers /{slug} there with the redirect, and anything else with a 404. Nil for every other host, so the app carries on as normal, and for the dashboard's own paths.

The link domains, read at most every 30 seconds.

Handles {link_path}/{slug} on the app's own domain: a redirect to the link's destination, with the click recorded, or a 404.

The mail service: from code, or as saved in the dashboard. Nil when there is none.

An instance from its options, with its state in a table owned by the calling process. In an app, start one under the supervision tree instead ({Runlight, opts}); this is for tests and scripts.

The clock, in epoch milliseconds.

Records a request from a known AI agent. Call it from middleware for every page request (see Runlight.Plug.Observer); it ignores everything else and never raises. at is when the page was served, for a log reader.

Answers a request for a short link: a redirect to its destination, with the click recorded like a visit but kept out of visitor and pageview counts. domain is the link domain the request came in on, or "" for the app's own link path, which answers for every link. Nil when no link fits.

The install a site is read from, when it is counted elsewhere: url, token, site, hostnames, scope.

What a connected install says about its site: its last visit and how long it keeps visits, asked at most once a minute, and whether it answered.

When a connected install's site last had a visit, asked at most once a minute.

How many months of visits a site keeps, or nil to keep everything (the default).

The oldest moment a site keeps visits from, or nil when it keeps everything.

The choices for how long a site keeps its visits, in months.

The routes: the dashboard, the API, and the tracker (see Runlight.Routes).

Saves the assistant's settings; an empty key keeps the one saved for the same provider. Nil removes them.

Saves the mail service from the dashboard. A secret field left blank keeps the saved value, so the browser never needs to see it.

Sends one email through the mail service: %{to, subject, html, text, headers}.

Sends every report that is due: last week's on Monday from 8am, last month's on the 1st, in each site's timezone. Safe to run often; each period goes out once.

Thirty minutes without a request ends a session.

Sets how long a site keeps its visits, in months, or nil for ever; a shorter time deletes visits after the answer.

The site with an id, or the first site when the id is nil or empty.

The site a page belongs to, or nil if it belongs to none.

The sites, with any settings changed in the dashboard applied.

Changes a site's name or timezone from the dashboard, kept apart from the settings in code. A managed site has no settings in code, so its changes, hostnames too, go to its row.

Types

t()

@type t() :: %Runlight{
  fetcher: term(),
  geo: term(),
  limit: term(),
  link_path: term(),
  local_installs: term(),
  mail: term(),
  managed_sites: term(),
  name: term(),
  now: term(),
  secret: term(),
  store: term(),
  table: term(),
  trust_proxy: term(),
  warn_direct: term()
}

Functions

add_site(rl, input)

@spec add_site(t(), Runlight.JS.Object.t()) :: Runlight.JS.Object.t()

Adds a site, when sites are managed in the dashboard: one counted here, or one connected from another install.

assistant_settings(rl)

@spec assistant_settings(t()) :: Runlight.JS.Object.t() | nil

The dashboard assistant's provider, model, and key, kept sealed like the mail keys. Nil until an owner sets it up.

build_rollups(rl)

@spec build_rollups(t()) :: non_neg_integer()

Adds up each site's finished days, so long ranges read a row a day instead of every visit. A day is built two hours after it ends in the site's timezone, and at most ten days a run.

check(rl)

@spec check(t()) :: Runlight.JS.Object.t()

Scheduled upkeep, safe to run every minute. It rotates salts, sends the email reports that are due, deletes visits past each site's retention, and builds daily rollups. It also rereads sites, their dashboard settings, and connected installs, so a change made by another process shows up here too.

client_ip(rl, request)

@spec client_ip(t(), Runlight.Http.Request.t()) :: String.t()

The visitor's address, for the daily visitor hash and the rate limit. Behind a proxy it comes from a header: by default the last X-Forwarded-For entry, which the nearest proxy wrote, then X-Real-IP and CF-Connecting-IP.

collect(rl, request)

@spec collect(t(), Runlight.Http.Request.t()) :: :ok

Handles one tracker request. Always succeeds; bad input is dropped quietly.

delete_site(rl, id)

@spec delete_site(t(), String.t()) :: :ok

Deletes a site and everything recorded for it, when sites are managed in the dashboard.

deliver_report(rl, r, site, period \\ nil)

@spec deliver_report(t(), Runlight.JS.Object.t(), Runlight.JS.Object.t(), map() | nil) ::
  :ok

Builds and sends one report. Also used by "Send a sample now".

email?(value)

What passes for an email address: something@somewhere.tld, with no spaces, quotes, or angle brackets.

env(name)

@spec env(String.t()) :: String.t() | nil

An environment variable, trimmed, or nil when it is empty.

fetch(runlight, url, opts \\ [])

@spec fetch(t(), String.t(), keyword()) ::
  {:ok, Runlight.Http.Response.t()} | {:error, term()}

An outgoing request through the instance's fetcher (see Runlight.Fetch).

forget_remote_info(rl, id)

Forgets what a connected install said, after a change made through it.

idle(rl)

@spec idle(t()) :: :ok

Retention work still running; the scheduled check and tests wait for it.

init(rl)

@spec init(t()) :: :ok

Creates tables and records the configured sites. Runs once.

instance(name \\ __MODULE__)

@spec instance(atom()) :: t()

The instance started under name in a supervision tree.

mail_settings(rl)

@spec mail_settings(t()) :: Runlight.JS.Object.t() | nil

The mail service: from code, or as saved in the dashboard. Nil when there is none.

new(opts)

@spec new(keyword()) :: t()

An instance from its options, with its state in a table owned by the calling process. In an app, start one under the supervision tree instead ({Runlight, opts}); this is for tests and scripts.

now(runlight)

@spec now(t()) :: integer()

The clock, in epoch milliseconds.

observe(rl, request, at \\ nil)

@spec observe(t(), Runlight.Http.Request.t(), number() | nil) :: boolean()

Records a request from a known AI agent. Call it from middleware for every page request (see Runlight.Plug.Observer); it ignores everything else and never raises. at is when the page was served, for a log reader.

redirect(rl, request, slug, domain)

@spec redirect(t(), Runlight.Http.Request.t(), String.t(), String.t()) ::
  Runlight.Http.Response.t() | nil

Answers a request for a short link: a redirect to its destination, with the click recorded like a visit but kept out of visitor and pageview counts. domain is the link domain the request came in on, or "" for the app's own link path, which answers for every link. Nil when no link fits.

remote(rl, id)

@spec remote(t(), String.t() | nil) :: Runlight.JS.Object.t() | nil

The install a site is read from, when it is counted elsewhere: url, token, site, hostnames, scope.

remote_info(rl, id)

@spec remote_info(t(), String.t()) :: map() | nil

What a connected install says about its site: its last visit and how long it keeps visits, asked at most once a minute, and whether it answered.

remote_last_seen(rl, id)

When a connected install's site last had a visit, asked at most once a minute.

retention(rl, site)

@spec retention(t(), String.t()) :: integer() | nil

How many months of visits a site keeps, or nil to keep everything (the default).

retention_cutoff(rl, site)

@spec retention_cutoff(t(), String.t()) :: integer() | nil

The oldest moment a site keeps visits from, or nil when it keeps everything.

retention_months()

The choices for how long a site keeps its visits, in months.

routes(rl, opts \\ [])

@spec routes(t(), keyword()) :: Runlight.Routes.t()

The routes: the dashboard, the API, and the tracker (see Runlight.Routes).

save_assistant_settings(rl, input)

@spec save_assistant_settings(t(), Runlight.JS.Object.t() | nil) :: :ok

Saves the assistant's settings; an empty key keeps the one saved for the same provider. Nil removes them.

save_mail_settings(rl, input)

@spec save_mail_settings(t(), Runlight.JS.Object.t() | nil) :: :ok

Saves the mail service from the dashboard. A secret field left blank keeps the saved value, so the browser never needs to see it.

send_mail(rl, message)

@spec send_mail(t(), map()) :: :ok

Sends one email through the mail service: %{to, subject, html, text, headers}.

send_reports(rl)

@spec send_reports(t()) :: %{sent: non_neg_integer(), failed: non_neg_integer()}

Sends every report that is due: last week's on Monday from 8am, last month's on the 1st, in each site's timezone. Safe to run often; each period goes out once.

session_idle_ms()

Thirty minutes without a request ends a session.

set_retention(rl, site, months)

@spec set_retention(t(), String.t(), integer() | nil) :: :ok

Sets how long a site keeps its visits, in months, or nil for ever; a shorter time deletes visits after the answer.

site(rl, id)

@spec site(t(), String.t() | nil) :: Runlight.JS.Object.t() | nil

The site with an id, or the first site when the id is nil or empty.

site_for(rl, hostname, id \\ nil)

@spec site_for(t(), String.t(), String.t() | nil) :: Runlight.JS.Object.t() | nil

The site a page belongs to, or nil if it belongs to none.

sites(rl)

@spec sites(t()) :: [Runlight.JS.Object.t()]

The sites, with any settings changed in the dashboard applied.

update_site(rl, id, patch)

@spec update_site(t(), String.t(), Runlight.JS.Object.t() | map()) ::
  Runlight.JS.Object.t()

Changes a site's name or timezone from the dashboard, kept apart from the settings in code. A managed site has no settings in code, so its changes, hostnames too, go to its row.