Releases: OpenLabs-so/openanalytics
Release list
v0.5.0
Nothing to configure by hand. git fetch --tags && git checkout v0.5.0 && cd infra/selfhost && ./upgrade.sh; on Coolify or Dokploy, redeploy with the new tag. ClickHouse migration 0022 runs through the ordinary migrate one-shot and drops the now-unused events_preview table.
Removed (0.x: a minor may remove)
- Ingest test mode and the no-code rule preview are gone (ADR-0068). The client-claimed
test_modefield is accepted and ignored; the preview token, its mint route,?oa_preview=and theevents_previewtable are removed. All traffic is ordinary: visible and billable. Staging belongs on its own site with its own tracking key. An existing key volume may still hold apreviewkeypair; nothing reads it.
New
- Dokploy support, measured. A one-service Compose blueprint (
infra/selfhost/dokploy/) and an operator guide written from a live install: DOKPLOY.md. Fourteen containers, four domains on a header-stripping Caddy edge, every secret generated for you. The catalogue submission to Dokploy's templates is in flight; until it lands, the guide's import path works today. - The setup flow no longer holds you hostage. Onboarding order is now site, timezone, install, verify, and the verify step offers "I'll do this later" instead of waiting on the first event.
- TanStack Start. An install guide (
/docs/install/tanstack-start) plusoa initdetection and a root-route injector, shipped to npm asgetopen@0.4.0. Contributed by @DeckardGer (#3, #5): the first external contribution to this repository, and a careful one.
Improved
- Realtime board: visitor trails are cached, modal cards no longer blink on refresh, and the placeholder card explains itself.
- The freshness chip sits with the tooltip it belongs to.
- Docs: the disk requirement reads 25 GB, which is the number an upgrade actually needs.
v0.4.2
Coolify only. Your visitors get a country now. Redeploy from the platform and
the first deploy downloads a geo database. The images are byte-identical to
v0.4.0 and this tag runs them; an ordinary docker-compose.yml install is
untouched and always had --with-geoip.
What was wrong
Every Coolify install reported a null country for every visitor, on every
plan, forever. The geoip volume started empty, there was no checkout to fill it
from, and GEOIP_DB_PATH was deliberately unset because a path that names no
file is a refusal to start.
Each of those was true. None of them was an acceptable place to leave a
self-hoster. It was written down in two places, and the Coolify half of the
instruction pointed at infra/selfhost/geoip/, a directory that does not exist
on that platform.
What it does now
A geoip one-shot does what fetch-dbip.sh does for a host with a shell: on the
first deploy it downloads DB-IP City Lite, checks the file really is a
MaxMind-format database, and moves it into place. The collector's
GEOIP_DB_PATH now points at it.
The licence is what makes fetching on your behalf possible at all. GeoLite2
needs a MaxMind account and a licence key and forbids redistribution, so nothing
could have fetched it for you. DB-IP City Lite is
CC BY 4.0: a direct download, no
account, with an attribution condition. That condition asks for credit wherever
you show the data:
IP Geolocation by DB-IP, https://db-ip.com, used under
CC BY 4.0.
The step prints it too.
The network is a first-install dependency only. The file is written once and
never rewritten, so every later deploy exits on the first line without touching
the network. To refresh it, delete dbip-city-lite.mmdb from the volume and
redeploy; the databases go stale within a month or two. To use MaxMind instead,
copy your own file in and point GEOIP_DB_PATH at it.
A failed download fails the deploy. The collector reads that path at boot and
refuses to start on a file that is not there, so the alternative is a crash loop
one step further from its cause. Redeploy to retry, or install with no geo by
deleting the service, its depends_on entry and the collector's GEOIP_DB_PATH.
COOLIFY.md says so.
Proven before it shipped
On a real host, with the command rendered exactly as Compose hands it to the
shell: 124 MB written, mode 0444, owned by uid 1000; a second run left it
alone; and the published collector image started against it with geoip_loaded, configured: true.
One thing the host script could not have told us. Its verification is
grep -qa MaxMind.com, which is correct under GNU grep and finds nothing under
busybox. Measured on the real 130 MB database: grep -qa said no,
strings | grep -q said yes. Without catching that, a truncated download would
have been caught one deploy later, by a collector refusing a file it could not
parse.
The install is now thirteen containers
Three of them exited, and that is success: migrate, keygen and geoip are
one-shot steps that do their work and stop.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.4.2, with
contents unchanged from v0.4.0. The tag moves because the compose file ships
with them and a release is one commit.
Full changelog: v0.4.1...v0.4.2
v0.4.1
Coolify only, and it is the tab you could not find. If your install has no
Account → Deployment screen, this is why. Redeploy from the platform and it
appears.
The images are byte-identical to v0.4.0 and this tag runs them. Nothing here
touches an ordinary docker-compose.yml install, which never had the bug.
What was wrong
OA_CREDENTIAL_KEYRING was filled from Coolify's SERVICE_BASE64_KEYRING. That
generator produces 32 base64 characters, which decode to 24 bytes. The
keyring wants a key of exactly 32 bytes, and credential-vault.ts checks the
length rather than trusting it, so every install came up with:
revenue_not_mounted reason: OA_CREDENTIAL_KEYRING invalid_key_material
The keyring is also what the deployment-settings surface encrypts with, and that
surface closes without one. So the Account → Deployment tab was never drawn:
no mail relay, no assistant provider, no revenue connections, and nothing on
screen saying why. Measured on a live install before it was changed: 32
characters in, 24 bytes out.
Why the fix is a generator and not a longer variable
No magic variable on the platform produces 32 bytes. SERVICE_BASE64_ is 32
characters, SERVICE_BASE64_64_ is 48 bytes, SERVICE_BASE64_128_ is 96. Base64
of 32 bytes is 44 characters and there is no _44_.
So the keyring joins the three signing pairs, which are there for exactly the
same reason: keygen writes it with openssl rand -base64 32, only when it is
missing, and the api and the worker read it through
OA_CREDENTIAL_KEYRING_FILE. Never rewriting an existing one is the load-bearing
part. Rotating a keyring on a redeploy would strand every provider credential a
customer had already connected.
Proven before it shipped
On a real host, with the command rendered exactly as Compose hands it to the
shell:
- the keyring it wrote is 44 base64 characters, 32 decoded bytes, mode
0400, owned by uid 1000; - a second run left all three key pairs and the keyring alone;
- the published
v0.4.0api image started against it and logged
revenue_mounted, active_key_version: k1.
Also in this tag
COOLIFY.md stops claiming the branch can be changed in Settings. On Coolify
4.3.2 that field does not appear after the first deploy either. It does not need
to: the compose file pins the images, not the checkout. Every image: line reads
${OA_IMAGE_TAG:-v0.4.1}, so a clone of main runs the release named in the
file it just cloned.
The wiring test now accepts X_FILE as satisfying X. A path is another way
to supply any declared variable, and supplying both is an error, so the two
spellings are alternatives. A test that demanded the inline one would force the
wrong fix on exactly the values a platform cannot express.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.4.1:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. Their contents are unchanged
from v0.4.0; the tag moves because the compose file ships with them and a
release is one commit.
Full changelog: v0.4.0...v0.4.1
v0.4.0
If you installed on Coolify, your tracker script was never being served. Upgrade,
and check it:
curl -o /dev/null -w '%{http_code}\n' https://c.<your-domain>/oa.js # 200, not 404An ordinary docker-compose.yml install was never affected and needs nothing beyond
the usual:
git fetch --tags && git checkout v0.4.0
cd infra/selfhost && ./upgrade.shThe bug, and why nothing caught it
tracker-build is the shared Dockerfile's build stage. That stage copied apps,
packages and the manifests, and not scripts/ — while pnpm run tracker:build
is node scripts/build-tracker.mjs. The stock compose never noticed, because it
bind-mounts that directory from your checkout and passes an explicit command.
A one-click platform install has neither. So the container inherited the build
stage's default command, which is a bare node: it read EOF from a closed stdin and
exited 0. A one-shot reporting success, with the shared volume left empty and
nothing written anywhere to say so.
Nothing would have served the file even if it had been built. The collector mounts
/v1/tracker, /v1/events and /v1/realtime and no static route; on the stock
compose Caddy serves /oa.js from the volume, and the platform variants have no
Caddy. Meanwhile the dashboard hands every new site
<script src="${COLLECTOR_BASE_URL}/oa.js">.
The result: a Coolify install from v0.3.0 through v0.3.2 had a working dashboard, a
working api and working ingest, and a snippet that could not load. The end-to-end
proof that shipped with those releases passed because it posted events directly to
/v1/events and never fetched the script.
Both halves are fixed
The build. The build stage copies scripts/, builds the bundle, and asserts it is
non-empty. A missing bundle now fails where the image is built rather than where it
is served, which is the same rule the APP guard beside it has always followed.
The serving. apps/collector/src/tracker-script.ts reads the bundle once at boot
and answers GET /oa.js with the headers Caddyfile:93-114 already set, for the same
reasons: one hour of freshness, an ETag that revalidates to a bodyless 304, and
the copy precompressed once at startup so the bytes on the wire are the bytes the
size budget measured. The two encodings get different tags, so a shared cache cannot
hand gzip to a client that asked for none.
With no bundle the route is not mounted at all. An empty 200 is the worst answer
available here: every visitor's browser would cache "the tracker is nothing" for an
hour. A 404 is a miss an operator can see.
What changed for whom
- Stock compose: nothing. Caddy still serves
/oa.jsfrom the volume and never
forwards it, so the new route is unreachable there.tracker-buildand the
trackervolume stay exactly as they were. - Coolify and the other one-click platforms:
docker-compose.coolify.ymldrops
tracker-buildand thetrackervolume, which now do nothing. That install pulls
nine images instead of ten and ends with twelve containers, two of them
exited rather than thirteen and three.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.4.0: migrate,
tracker-build, api, collector, worker, query-gateway, realtime, web,
clickhouse, valkey. amd64. On arm64, build them instead: same compose file, one
flag.
Every one of them now carries the tracker bundle, because the runtime stage copies
the build stage wholesale and ten kilobytes is not worth a second stage to keep out
of the five that do not serve it.
A minor rather than a patch, by RELEASING.md:
the shape of a compose file an operator may have overridden changed.
Full changelog: v0.3.2...v0.4.0
v0.3.2
A patch, and for the ordinary compose path a documentation one: nothing here
changes what a docker-compose.yml install runs.
git fetch --tags && git checkout v0.3.2
cd infra/selfhost && ./upgrade.shAn install guide written from an install
infra/selfhost/COOLIFY.md is now step by step, and every step in it is one that
was got wrong on a real Coolify before it was written down. It came out of a
second install done from a zero-state Coolify, by hand, through the interface,
by somebody who had not deployed this stack before.
What that turned up, all of it now in the guide:
- "Public Repository", not "Docker Compose". The resource type called Docker
Compose gives you an empty editor to paste a file into. The repository flow is
the one that clones a tree, and it asks for the build pack afterwards. - Three of Coolify's own guesses are wrong here, and the branch cannot be set
until the resource exists. The guide names all four fields and what they become. - Every domain row must be
https. A row left onhttpis served without a
certificate and none is ever requested, while the container behind it stays
healthy. That is the failure shape this whole file exists to warn about. - The port box should fill itself from
expose:, and once it did not. If one
is empty, type it. An empty port means the router is dropped as unroutable, no
certificate is requested, and nothing is logged. - Coolify keeps two copies of every variable,
ProductionandPreview. The
list does not make the difference obvious, a deploy reads Production, and
editing the wrong one is silent. - Its DNS check can be wrong on records that are right. Verify with
digand
continue.
One fix, and it is the one that made a first deploy impossible
EMAIL_FROM shipped as ${OA_EMAIL_FROM:-}. The schema wants at least three
characters, so the worker refused to start, restarted, and refused again, over a
variable nothing told the operator was required. env/worker.env.example has
shipped a placeholder there the whole time; the Coolify file had dropped it.
It is a from-address and not a lock. Mail is configured later from the dashboard
under Account then Deployment, and a relay stored there wins over
anything in the environment, so magic links work whatever this says.
The guard is a test: no ${VAR:-} anywhere in that file. A variable the platform
fills in is written with no default at all, which is the honest spelling.
A note for the ordinary compose path
Not a change in this release, but the one thing v0.3.0 changed for an existing
install, now said where somebody will read it rather than only in a comment.
clickhouse and valkey became our own images in v0.3.0, with their config
files baked in instead of bind-mounted. The files have not moved and are what
the images are built from. What changed is that editing one in place no longer
takes effect: build instead of pull (./upgrade.sh --from-source), or
bind-mount your version over the baked one, which still wins.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.3.2:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. On arm64, build them instead:
same compose file, one flag.
Their contents are unchanged from v0.3.1. The tag moves because the compose
file, the env templates and the migrations ship with the images and a release is
one commit.
RELEASING.md
is what a version number here means.
Full changelog: v0.3.1...v0.3.2
v0.3.1
A patch, and for the ordinary compose path an empty one: nothing here changes
what a docker-compose.yml install runs. Everything below is the Coolify
variant, which went from a file that had been read to a stack that has been run.
git fetch --tags && git checkout v0.3.1
cd infra/selfhost && ./upgrade.shThe Coolify variant has now been deployed, and it took five fixes
infra/selfhost/docker-compose.coolify.yml was written by reading, and every one
of these was invisible until it ran on Coolify 4.3.2 against real DNS and real
certificates.
- The migration one-shot did not run migrations. It is the worker's image
under another tag, so with no entrypoint of its own it booted as the worker,
loaded the worker's schema, and was refused by the least-privilege boundary:
CLICKHOUSE_MIGRATION_PASSWORD: must not be provided to the "worker" service.
The boundary was right; the file was missing a line the stock compose file has
always had. - The ClickHouse database was named wrong.
openanalytics, where every grant
inclickhouse/oa-entrypoint.shis written againstanalytics.<table>by name
rather thananalytics.*. The stack would have come up green and failed on the
first read with a permission error naming a table nobody had renamed. GEOIP_DB_PATHwas set on a volume that starts empty. A path is a promise:
the collector reads it at boot and refuses to start when the file is not there.
On a platform with no checkout to download into, that turned the documented
degradation into a crash loop. Unset is what an install with no database
should say.- The dashboard was probed on a route it does not serve. The shared
healthcheck asks for/health; the six backend services answer it and a
Next.js app does not. Sowebanswered404forever, never turned healthy,
and the proxy served "no available server" in front of a dashboard whose own
log saidReady in 283ms. - The public services did not say which port they listen on. Traefik takes a
container's port from the ports it exposes, and the six backend services share
a Dockerfile that declares none. The collector's router was therefore
unroutable, was dropped, and its certificate was never even requested. Nothing
logged it: the hostname simply did not answer while the container beside it was
healthy.expose:publishes nothing to the host and is exactly the metadata
these platforms read, so declaring it fixes every one of them at once rather
than asking an operator to type a port they have no way to know.
What now holds on a live install, rather than in a file: all thirteen
containers correct, four hostnames on HTTPS with Let's Encrypt certificates, an
account and a site created through the dashboard, and three page views accepted
by the collector, drained through the queue by the worker and stored in
ClickHouse.
The client-identity middleware works, and that is the one that mattered. Every
forged header is deleted before it reaches the collector: CF-Connecting-IP,
CF-IPCountry, CF-IPCity, True-Client-IP, Fly-Client-IP,
Fly-Client-Country, X-Vercel-IP-Country, X-Vercel-IP-City, with
X-Forwarded-For left carrying the real connection address. Proven twice: once
against an echo service behind the same middleware, and once by sending
CF-IPCountry: XX with a real page view and finding the stored row's country
empty rather than XX.
Still not a supported path, because one install on one platform is a
demonstration rather than a promise. It is no longer a file nobody has run.
A correction, and one that matters when you cut a release
RELEASING.md said a GHCR package is private when first created and needs a
visit to its settings page. That was true on v0.1.0 for the original eight. It
was not true on v0.3.0 for clickhouse and valkey, which came out public
with nothing clicked. Two data points and no announced change between them, so
the section now says to check rather than to believe either answer, and gives the
one command that checks it from a machine with no GHCR login.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.3.1:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. On arm64, build them instead:
same compose file, one flag.
Their contents are unchanged from v0.3.0. The tag moves because the compose
file, the env templates and the migrations ship with the images and a release is
one commit.
RELEASING.md
is what a version number here means.
Full changelog: v0.3.0...v0.3.1
v0.3.0
A minor, and the number means what it says: read this before upgrading.
./upgrade.sh still does the work and there is nothing to run by hand, but two
of the images you pull are new and one thing you may have edited moved.
git fetch --tags && git checkout v0.3.0
cd infra/selfhost && ./upgrade.shTen images, not eight
clickhouse and valkey are now ours. They are upstream's images plus the files
they cannot start correctly without: ClickHouse's config drop-in and the
entrypoint that renders its four least-privilege users, and Valkey's two policy
files and its entrypoint. Those five files used to arrive as bind mounts from
infra/selfhost/.
Why this changed. A bind mount works when the compose file sits in a
checkout and nowhere else. A one-click platform clones the repository, writes the
rendered compose file and nothing beside it, so Docker creates an empty
directory for every missing bind source and ClickHouse dies with
oa-entrypoint.sh: Is a directory. Measured on Coolify 4.3.2. Every platform in
that category behaves the same way, so this was the last thing between the
project and a marketplace listing.
The files still live where they did, in clickhouse/ and valkey/, and are
copied into the images at build. There is one of each in the tree.
If you edited one of those files in place, it now has no effect. Two ways
back: bind-mount over the baked copy, which still wins, or build instead of pull
(./upgrade.sh --from-source). The five files are exactly the ones listed above;
nothing else moved.
One image serves both Valkeys. The queue and the realtime cache are the same
server under opposite policy, so both configs ship in it and OA_VALKEY_CONF
names which applies. A container started without it refuses rather than guesses:
a cache policy on the queue would evict acknowledged events before the worker
inserted them.
The plaintext Valkey check accepts a service name
buildConnectionOptions refuses a plaintext redis:// whose host it cannot
prove is private, and it proved that by matching the hostname against the RFC1918
literals. A compose service name matches none of them, so the check had no answer
for a name and refused every one.
It now also accepts a single-label name such as valkey-queue: a label with
no TLD has no public resolver that can answer it. A host with a dot in it outside
the private ranges is still refused, TLS is still required where the hop may be
public, and AUTH is still required everywhere. This is what lets a platform that
allocates its own network run the stack, since none of them lets a template pin
an address inside it.
The caveat, stated rather than hidden: a host configured with a DNS search suffix
can resolve a bare label to something off the machine. That needs an operator who
set a search domain and then wrote a bare label into a Valkey URL.
Coolify
infra/selfhost/docker-compose.coolify.yml has now been run on a real Coolify
install rather than only read. Two things that could not be checked any other way
both hold: the client-identity middleware is genuinely attached to the router
Coolify generates, and the SERVICE_FQDN_*, SERVICE_PASSWORD_* and
SERVICE_BASE64_* variables are filled in from the compose file with nothing
typed by hand.
Still not a supported path, and it will say so until an install has been run end
to end and watched.
One difference to know about there: /geoip is a named volume that starts empty,
because that platform has no checkout to mount a database from. Geo is null until
you copy one in, which is the documented degradation rather than a failure.
SELF-HOSTING.md says how under GeoIP.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.3.0:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. On arm64, build them instead:
same compose file, one flag.
Migrations do not go down. ./upgrade.sh takes the snapshot ./rollback.sh
needs before it starts, and a rollback discards everything recorded after it.
RELEASING.md
is what a version number here means.
Full changelog: v0.2.1...v0.3.0
v0.2.1
A patch, which here means exactly one thing: ./upgrade.sh and nothing else.
No migration, no new variable, no change to the tracker.
git fetch --tags && git checkout v0.2.1
cd infra/selfhost && ./upgrade.shThe revenue switch now means what the screen says
v0.2.0 shipped the per-site switch that decides whether a payment is tied back to
the visit that produced it, and it closed the half a browser can reach: the
tracker stopped sending the order reference for a site with the switch off.
Two of the three signals that link a payment never pass through a browser. A
checkout reference and an identity on the payment object travel from a site's own
server to the provider and reach us on a webhook, so a site could switch linking
off, watch the change take effect in its own network tab, and still find journeys
appearing. A setting that describes the browser while the product does something
else is worse than no setting.
Both halves are closed here. The collector drops a linking hint that arrives for
a site with the switch off, whatever sent it, and counts what it removed, so a
page still serving a bundle cached from before the flag existed is visible rather
than silent. The attribution job reads the switch once, above every read it
gates, so a fourth signal cannot be added past it by accident later.
Three things this deliberately does not do:
- Revenue figures do not move. Totals, per-day, per-hour, refunds, disputes
and fees are read from the payments themselves. The switched-off path still
runs the rollup and still advances the watermark, and a test asserts the
buckets written are identical with the switch in either position. - Nothing already recorded is deleted. Journeys from before the switch went
off stay in the dashboard. Turning a setting off is not an erasure request. - Turning it back on reaches back a month, not further. The matcher re-reads
roughly the last 31 days on its next pass, which is the horizon it already had.
The settings screen and the Stripe connect flow now say all three, because
somebody deciding this needs to know that their revenue is not what is at stake.
/docs/revenue carries the same account.
Self-hosting, corrected from a real run
infra/selfhost/docker-compose.keys.yml was documented as unverified. It has now
been run on a real host: keys are generated once and a redeploy does not rotate
them, the modes and ownership hold on fresh volumes, and a service started before
its key exists refuses to start with the reason on the line and then recovers on
its own. Two behaviours are written down rather than left to be discovered:
- Services waiting on the one-shot
keygenstep can be leftCreatedrather
than started once it finishes. A seconddocker compose up -dstarts them. - The two Valkey addresses are pinned rather than allocated, so a recreate can
ask for an address the container it replaces is still holding and stop with
Address already in use. Run the same command again. Deleting the
ipv4_addresslines is the wrong fix: the connection factory rejects a
plaintextredis://on a service name, and the queue would not come up at all.
Images
Eight, published to ghcr.io/openlabs-so/openanalytics and tagged v0.2.1:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web. amd64. On arm64, build them instead: same compose file, one
flag.
infra/selfhost/docker-compose.coolify.yml moves its pin to v0.2.1 with the
tree. Still not a supported path, and now for a named reason rather than an
unexplored one: without the compose network it has no private address to give
the Valkey URLs, and the connection check refuses a plaintext redis:// on a
service name. The file says so at the top, with the three ways out.
Migrations do not go down. ./upgrade.sh takes the snapshot ./rollback.sh
needs before it starts, and a rollback discards everything recorded after it.
RELEASING.md
is what a version number here means.
Full changelog: v0.2.0...v0.2.1
v0.2.0
A minor, and the number means what it says: read this before upgrading.
./upgrade.sh still does the work and there is nothing to run by hand, but two
things change on their own once it has run.
git fetch --tags && git checkout v0.2.0
cd infra/selfhost && ./upgrade.shTwo things that change by themselves
Linking a payment to a visitor is now a per-site switch, and it is off by
default. Two Postgres migrations add it and backfill it. A site that already
had a revenue provider connected is read as having opted in already, keeps its
switch on, and keeps linking payments to journeys exactly as before. A site that
disconnected its provider, and every site created from now on, starts with it
off. Nothing to answer, and nothing to run.
The tracker changes, so it reaches every visitor of every site within the
hour. oa.js is unversioned and cached for an hour by design. This version
sends the revenue linking hint only for a site whose switch is on, and it gains
a strict storage mode: data-storage="none" runs it memory-only, with no read
of or write to localStorage or sessionStorage.
Revenue totals, or revenue attributed
Connecting Stripe now asks which one you want, as its own step, and the same
choice lives in Settings under Integrations so it can be changed later.
Revenue totals is the default. Nothing is asked of your visitors, and
totals, refunds, disputes and fees all work. Journeys stay empty of money.
Attributed revenue ties a payment back to the visit that produced it. Your
pages send an order reference with the conversion, and that is what turns a
count into a link. It carries a consent obligation you meet as your site's
controller: the script does not hold the hint back until somebody answers, so
wiring oa.consent("granted") on your own checkout is yours to do. The screen
says so and hands you the line.
Changes are saved as soon as you make them. Visitors' browsers hold their copy
of a site's configuration for about thirty seconds, so a change reaches them a
moment later rather than instantly.
Secrets may arrive as file paths
Every variable the schemas declare now also accepts X_FILE, naming a file
whose contents become the value of X. Existing deployments are unaffected:
set no _FILE variable and nothing changes.
Three rules, all of them loud. A path is a promise, so an unreadable or empty
file is a refusal to start, unlike an absent variable which several services
treat as "this feature is off". Setting X and X_FILE together is a startup
error rather than a precedence rule. And a path resolves before the
least-privilege check, so pointing a service at a secret it is not allowed to
hold fails exactly the way pasting that secret into its environment has always
failed.
This exists because one-click platforms can generate a random string and nothing
else, and none of them can produce a keypair whose halves must match across two
services. infra/selfhost/docker-compose.keys.yml is the worked example: a
one-shot step that makes the three signing pairs inside the stack and writes each
half into its own volume, so the api holds the private halves and each verifier
sees only the public one it needs.
It has been run on a real host. Keys are generated once and never rotated on a
redeploy, the modes and ownership hold on fresh volumes, and a service started
before the keys exist refuses to start with the reason on the line and recovers
on its own rather than wedging.
infra/selfhost/docker-compose.coolify.yml is the first platform variant, for
Coolify and anything else that owns the proxy. Not yet a supported path: it has
not been run on a live Coolify install.
Also fixed
The percentages under the chart were measured against the largest row in
Locations, Sources and Devices, so the top entry always read 100%. They are a
share of the rows shown now, and blank rather than wrong when the row set was
capped.
The published privacy notice was corrected: the four short-lived values the
script keeps on a device are named, the ePrivacy basis is stated as first-party
audience measurement with its cumulative conditions, and the two features that
leave that envelope say plainly that the consent obligation is the site's.
The realtime visitor panel names the page a visitor is on, and an empty trail no
longer blames ingest lag for it.
Images
Eight, published to ghcr.io/openlabs-so/openanalytics and tagged v0.2.0:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web. amd64. On arm64, build them instead: same compose file, one
flag.
Migrations do not go down. ./upgrade.sh takes the snapshot ./rollback.sh
needs before it starts, and a rollback discards everything recorded after it.
RELEASING.md
is what a version number here means.
Full changelog: v0.1.1...v0.2.0
v0.1.1
A patch. Two of these are wrong numbers on a dashboard somebody was already
reading, which is the kind of bug that does not announce itself.
To upgrade: ./upgrade.sh and nothing else. No new configuration, no
migration to run by hand, no setting that changes meaning.
git fetch --tags && git checkout v0.1.1
cd infra/selfhost && ./upgrade.shFixed
The percentages under the chart were measured against the wrong number.
Locations, Sources and Devices divided each row by the largest row rather than
by the total, so the top entry always read 100% and everything under it was
overstated. They are now a share of the rows shown, and blank rather than wrong
when the gateway capped the set. Top pages was already correct, which is why
this looked like an inconsistency between cards rather than a bug.
A visitor's journey no longer splits at UTC midnight. The anonymous identity
rotates daily by design, and two ids that share a client session hint are the
same visitor whatever the calendar says. The trail and the session bridge now
agree on that rule, so a session that crosses midnight is one journey instead of
two visitors.
The realtime visitor panel says which page somebody is on, and an empty
trail no longer blames ingest lag for it. Presence is refreshed by any activity,
so a visitor can be here with nothing recorded in the last 24 hours; the panel
says that instead of implying something is broken.
The magic-link screen mentions the spam folder. It is where the first link
usually is.
Self-hosting
A release install no longer needs the two-line .env edit.
generate-secrets.sh reads the image tag out of the checkout it is standing in
and points .env at that release's images, printing which it picked and why. On
a branch, on main, or with no git at all it writes the build defaults instead.
The quickstart in README.md and SELF-HOSTING.md now resolves the newest
release on its own and skips release candidates, which git describe would
otherwise hand you: a candidate publishes images under its own tag and sorts
above the release it is a candidate for.
Images
Eight, published to ghcr.io/openlabs-so/openanalytics and tagged v0.1.1:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web. amd64. On arm64, build them instead: same compose file, one
flag.
RELEASING.md
is what a version number here means.
Full changelog: v0.1.0...v0.1.1