One tag, one SDK, one project.
Four things to wire up, none of them longer than a few lines. Everything reports into the same project and lands on the same dashboard.
Sources
Setup
Four lines of wiring, then it is reporting.
There is no pipeline to stand up and no warehouse to point at. The tag and the SDK talk to one endpoint, and the endpoint writes to one database.
- 01 web
Add the tag to the site
One script tag on the marketing site. Vanilla TypeScript, under 3KB gzipped, no dependencies and no second request. It writes nothing and sends nothing until consent is given.
<script defer src="https://…/t.js" data-key="fr_web_…"></script> - 02 app
Initialise the SDK in the app
One call at start-up. From there the same client carries product events, exceptions, logs and feedback, so there is one thing to configure rather than four.
firstrun.init({ key: "fr_app_…", release: "2.7.0", }) - 03 app
Send whatever you want to count
A typed envelope with your own name and properties. Exceptions and log lines go through the same path, so an event queued on a laptop with no network survives the restart and arrives later.
firstrun.track("project.created", { template: "blank", }) - 04 app
Drop in the feedback widget
A reporter that attaches the running release, the platform and the last exception before it was opened, so what lands is a ticket rather than the start of a conversation about which build they are on.
firstrun.feedback.open()
Recording
How a thing that happened becomes a row.
The rules below are enforced by tests rather than by convention, which is the only reason they are worth printing on a marketing page.
-
Client time is the real time
A laptop is offline, the OS kills the process, the queue replays three days later. The event still lands in the day it happened. Nothing is bucketed on arrival time.
-
One envelope for everything
A product event, an exception and a log line are the same shape with a different type. That is why one SDK call configures all of them, and why the retry queue does not care which is which.
-
The release travels with the event
Every record carries the version it came from, so adoption, retention and crash rate are all cut by the same field rather than reconstructed from timestamps afterwards.
-
Ingestion fails closed
A plan that runs out or a key that is wrong stops ingestion and says so. It does not sample quietly, and it never deletes what has already landed.
Privacy
Small enough to hold, on purpose.
The less a tag has to collect, the fewer promises there are to keep. This one collects little enough that self-hosting removes the last of them.
-
Nothing before consent
The web tag holds no storage and sends no request until consent is given. Refusing it is a no-op, not a degraded mode with a cookie in it.
-
IPs are hashed with a salt you own
A raw address is never written. Self-hosted, the salt lives in your environment and never leaves it, so the hashes are not reversible by us, because we do not have it.
-
No fingerprinting
No device fingerprint, no cross-site identifier, no session replay. A visitor id is a first-party value scoped to your own domain and nothing else.
-
No third parties, anywhere
No CDN, no font host, no tag manager. The page you are reading fetches its typeface from its own origin for exactly the same reason.