runlight 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/assets/build.json +3 -3
- data/assets/dashboard.js +7 -7
- data/assets/locales.json +5 -5
- data/lib/runlight/accounts/accounts.rb +15 -5
- data/lib/runlight/accounts/crypto.rb +2 -0
- data/lib/runlight/accounts/throttle.rb +10 -19
- data/lib/runlight/accounts/web.rb +23 -1
- data/lib/runlight/core.rb +69 -31
- data/lib/runlight/http/fetcher.rb +1 -1
- data/lib/runlight/importers/client.rb +6 -6
- data/lib/runlight/importers/index.rb +3 -2
- data/lib/runlight/importers/visits.rb +2 -2
- data/lib/runlight/payload.rb +18 -5
- data/lib/runlight/routes.rb +24 -5
- data/lib/runlight/safefetch.rb +20 -14
- data/lib/runlight/server/config.rb +3 -2
- data/lib/runlight/store/sql.rb +2 -0
- data/lib/runlight/store/sql_store.rb +16 -1
- data/lib/runlight/version.rb +1 -1
- metadata +2 -2
data/lib/runlight/safefetch.rb
CHANGED
|
@@ -80,22 +80,30 @@ module Runlight
|
|
|
80
80
|
return false if g.nil?
|
|
81
81
|
|
|
82
82
|
embedded = ->(hi, lo) { [hi >> 8, hi & 255, lo >> 8, lo & 255] }
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
end
|
|
88
|
-
return public_v4?(embedded.call(g[6], g[7])) if g[0] == 0x64 && g[1] == 0xff9b && zero.call(g[2, 4])
|
|
83
|
+
# IPv4 inside IPv6: mapped (::ffff:0:0/96), the old compatible form (::/96), translated (::ffff:0:0:0/96),
|
|
84
|
+
# and NAT64 (64:ff9b::/96).
|
|
85
|
+
inside = embedded_v4(g)
|
|
86
|
+
return g[5].zero? && g[4].zero? && g[6].zero? && g[7] <= 1 ? false : public_v4?(inside) unless inside.nil?
|
|
89
87
|
# 6to4 carries an IPv4 address in its second and third groups.
|
|
90
88
|
return public_v4?(embedded.call(g[1], g[2])) if g[0] == 0x2002
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
return false if g[0]
|
|
94
|
-
|
|
89
|
+
# Only global unicast (2000::/3) is public, which leaves out unique local, link-local, the old site-local,
|
|
90
|
+
# multicast, discard, and local-use NAT64 (64:ff9b:1::/48), which may carry any address.
|
|
91
|
+
return false if (g[0] & 0xe000) != 0x2000
|
|
92
|
+
# The protocol assignments (2001::/23, with Teredo and ORCHID) and the documentation prefixes.
|
|
93
|
+
return false if g[0] == 0x2001 && (g[1] < 0x200 || g[1] == 0xdb8)
|
|
94
|
+
return false if (g[0] & 0xfff0) == 0x3ff0
|
|
95
95
|
|
|
96
96
|
true
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# The IPv4 address inside a mapped, compatible, translated, or NAT64 IPv6 address, or nil.
|
|
100
|
+
def embedded_v4(g)
|
|
101
|
+
inside = (g[0, 5].all?(&:zero?) && (g[5] == 0xffff || g[5].zero?)) ||
|
|
102
|
+
(g[0, 4].all?(&:zero?) && g[4] == 0xffff && g[5].zero?) ||
|
|
103
|
+
(g[0] == 0x64 && g[1] == 0xff9b && g[2, 4].all?(&:zero?))
|
|
104
|
+
inside ? [g[6] >> 8, g[6] & 255, g[7] >> 8, g[7] & 255] : nil
|
|
105
|
+
end
|
|
106
|
+
|
|
99
107
|
# Names clouds give their metadata services.
|
|
100
108
|
METADATA_NAMES = %w[metadata metadata.google.internal metadata.goog instance-data instance-data.ec2.internal].freeze
|
|
101
109
|
|
|
@@ -105,10 +113,8 @@ module Runlight
|
|
|
105
113
|
def metadata_address?(ip)
|
|
106
114
|
four = v4(ip)
|
|
107
115
|
g = four.nil? ? v6(ip) : nil
|
|
108
|
-
# IPv4 inside IPv6, mapped
|
|
109
|
-
|
|
110
|
-
four = [g[6] >> 8, g[6] & 255, g[7] >> 8, g[7] & 255]
|
|
111
|
-
end
|
|
116
|
+
# IPv4 inside IPv6, mapped, compatible, translated, or NAT64, is read as IPv4.
|
|
117
|
+
four = embedded_v4(g) if four.nil? && !g.nil?
|
|
112
118
|
return (four[0] == 169 && four[1] == 254) || four == [100, 100, 100, 200] unless four.nil?
|
|
113
119
|
return false if g.nil?
|
|
114
120
|
return true if (g[0] & 0xffc0) == 0xfe80
|
|
@@ -16,7 +16,8 @@ module Runlight
|
|
|
16
16
|
# RUNLIGHT_SECRET signs sessions and encrypts saved keys (made and kept in DATA_DIR if unset)
|
|
17
17
|
# RUNLIGHT_TOKEN also accepted as a bearer token on the API, and makes the first account
|
|
18
18
|
# RUNLIGHT_URL the dashboard's public address, which can never become a link domain
|
|
19
|
-
# TRUST_PROXY
|
|
19
|
+
# TRUST_PROXY unset believes forwarded addresses only from a private network; "false" never,
|
|
20
|
+
# "true" from anywhere, or the one header a CDN sets, such as cf-connecting-ip
|
|
20
21
|
# RUNLIGHT_GEO city (the default), country, off, or the path to an MMDB file
|
|
21
22
|
# CRON_SECRET lets a scheduler run the check over HTTP, at POST /api/check
|
|
22
23
|
# RUNLIGHT_OBSERVE_KEY one key for every site's AI agent reports
|
|
@@ -115,7 +116,7 @@ module Runlight
|
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
# false with nothing in front, or the one header your proxy sets, such as cf-connecting-ip behind Cloudflare.
|
|
118
|
-
# Unset stays nil, so the library's default applies
|
|
119
|
+
# Unset stays nil, so the library's default applies: forwarding headers count only from a private address.
|
|
119
120
|
def trust_proxy
|
|
120
121
|
value = (get("TRUST_PROXY") || "").downcase
|
|
121
122
|
return nil if value == ""
|
data/lib/runlight/store/sql.rb
CHANGED
|
@@ -27,6 +27,8 @@ module Runlight
|
|
|
27
27
|
|
|
28
28
|
# The most visits journeys reads, newest first.
|
|
29
29
|
JOURNEY_VISITS = 20_000
|
|
30
|
+
# The most visits a funnel is counted over: the newest in the range, so its rows fit in memory over any range.
|
|
31
|
+
FUNNEL_VISITS = 100_000
|
|
30
32
|
|
|
31
33
|
# How long after a visit starts its events are looked for: far past any real visit.
|
|
32
34
|
EVENT_TAIL_MS = 2 * 86_400_000
|
|
@@ -25,6 +25,7 @@ module Runlight
|
|
|
25
25
|
class SqlStore
|
|
26
26
|
BOUNCE_MS = Sql::BOUNCE_MS
|
|
27
27
|
JOURNEY_VISITS = Sql::JOURNEY_VISITS
|
|
28
|
+
FUNNEL_VISITS = Sql::FUNNEL_VISITS
|
|
28
29
|
EVENT_TAIL_MS = Sql::EVENT_TAIL_MS
|
|
29
30
|
MYSQL_COLLATION = Sql::MYSQL_COLLATION
|
|
30
31
|
|
|
@@ -34,6 +35,8 @@ module Runlight
|
|
|
34
35
|
attr_reader :db,
|
|
35
36
|
# What the store's table and index names start with: `rl_` unless it was given another.
|
|
36
37
|
:prefix
|
|
38
|
+
# The most visits a funnel is counted over; smaller in tests, to try the cap.
|
|
39
|
+
attr_accessor :funnel_visits
|
|
37
40
|
|
|
38
41
|
# `prefix` takes the place of `rl_` at the start of every table and index name, so two apps can each keep
|
|
39
42
|
# Runlight's tables in one database. A database that is already prefixed keeps its own.
|
|
@@ -45,6 +48,7 @@ module Runlight
|
|
|
45
48
|
end
|
|
46
49
|
@db = db
|
|
47
50
|
@prefix = prefix
|
|
51
|
+
@funnel_visits = FUNNEL_VISITS
|
|
48
52
|
@ready = false
|
|
49
53
|
@checked_all = false
|
|
50
54
|
end
|
|
@@ -575,7 +579,18 @@ module Runlight
|
|
|
575
579
|
# How many visits reached each step, in order, within the same visit. Step
|
|
576
580
|
# one is the first matching row in the range; each later step must come
|
|
577
581
|
# after the step before it. Filters choose which visits enter the funnel.
|
|
582
|
+
# {"counts" => Array<Integer>, "sampled" => bool}
|
|
578
583
|
def funnel_counts(query, funnel)
|
|
584
|
+
# Past funnel_visits visits in the range, only the newest are counted: the range starts where the oldest
|
|
585
|
+
# of them began.
|
|
586
|
+
scope = Sql.visit_scope(query["filters"], query["site"], query["from"], query["to"], @db.dialect)
|
|
587
|
+
first = @db.all(
|
|
588
|
+
"SELECT COUNT(*) AS n, MIN(started_at) AS t FROM (SELECT s.started_at AS started_at FROM rl_sessions s WHERE s.site = ? AND s.started_at >= ? AND s.started_at < ? AND #{Sql::IS_VISIT}#{scope["sql"]}
|
|
589
|
+
ORDER BY s.started_at DESC, s.id LIMIT #{@funnel_visits + 1}) x",
|
|
590
|
+
[query["site"], query["from"], query["to"], *scope["params"]],
|
|
591
|
+
)[0] || {}
|
|
592
|
+
sampled = Sql.num(first["n"]) > @funnel_visits
|
|
593
|
+
query = query.merge("from" => [query["from"], Sql.num(first["t"]) + 1].max) if sampled
|
|
579
594
|
# The rows of the picked visits that match any step, in order, read once and walked here: a join from
|
|
580
595
|
# each step to the next is planned badly by Postgres, which cannot guess how many visits go on.
|
|
581
596
|
v = Sql.visit_rows(query["filters"], query["site"], query["from"], query["to"], @db.dialect)
|
|
@@ -611,7 +626,7 @@ module Runlight
|
|
|
611
626
|
reached += 1 if reached < counts.length && Sql.num(row["m#{reached}"]) == 1
|
|
612
627
|
end
|
|
613
628
|
close.call
|
|
614
|
-
counts
|
|
629
|
+
{ "counts" => counts, "sampled" => sampled }
|
|
615
630
|
end
|
|
616
631
|
|
|
617
632
|
# Each visit's pageviews in order, at most `per_visit` of them, for journeys.
|
data/lib/runlight/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Runlight
|
|
6
6
|
# The gem's own version, which the release keeps equal to the npm package's.
|
|
7
|
-
GEM_VERSION = "0.
|
|
7
|
+
GEM_VERSION = "0.3.0"
|
|
8
8
|
|
|
9
9
|
# The SDK's version and the HTTP API's, read from assets/build.json, which
|
|
10
10
|
# scripts/ruby-assets.mts writes from the TypeScript SDK, so the two always
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: runlight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon C. Phillips
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|