Repository navigation
Show owners who visits their lambda - #114
Merged
Merged
Conversation
An Analytics section in the editor, in both views: visitors, page views and requests per day, and the pages, referring sites, countries, devices, browsers, systems and crawlers behind them. Opt-in; switching it off deletes what was counted. Every request to a lambda is made into one LambdaVisit by the activity concern, which counts the overview's figures from it and leaves it on the request; the analytics concern reads it from there and puts it on a lock-free, bounded queue. A job writes the queue every five minutes into analytics.db, a star schema of its own (lambdas, and a fact table per statistic keyed by lambda, year, month and day), so nothing is written while a visitor waits. A year is kept; a free lambda shows seven days of it and a premium one all of it - a tier limit in the panel. Visitors are told apart for a day by a hash under a random daily key, held in memory only. Documented in the README, /docs, platform_guide, the new analytics MCP tool and the privacy page; translated into every language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s apart One concern, LambdaVisitConcern, makes the LambdaVisit of a request to a lambda or a preview and does nothing else. The overview's figures, the request log and the analytics read it off the request and leave out what is not theirs; nothing measures a request to a lambda twice. Bots are told from people by crawler-user-agents (MIT, maintained), kept in the release and fetched again daily, and counted by its kinds. Google, Bing, OpenAI, Apple and Perplexity are only named where the connection came from an address they publish - never a forwarding header's - and counted as an unconfirmed claim otherwise. Visitors are people only. Browsers, systems and devices come from the agent's tokens and Chromium's client hints, taken only where they name something known. Matching stays linear in an agent's length: the list's plain text is searched in one pass, its few expressions run without backtracking, and only 500 characters are read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Owners can now see who visits their lambda: an Analytics section in the editor (full view under Run, and in the simple view), opt-in, with visitors, page views and requests per day and the pages, referring sites, countries, devices, browsers, systems and crawlers behind them.
Screenshots
After the review: the crawler card names Google, Bing, OpenAI, Apple and Perplexity only when they were confirmed by their published addresses (check mark), and groups every other bot by its kind.
The first round:
The figures at the top are tabs for the chart below them. Each one shows the change against the period before, where the tier shows that period too. The chart has a column per day (per week for 12 months), with a hover tooltip and its values as a table one click away. The breakdowns are ranked bars. Devices are one bar of shares with a legend. Its four colours were checked for colour blindness in light and dark mode. The tab does not poll: it reads again when the page becomes visible.
How it works
LambdaVisitConcern(new folderServices/Visits) builds aLambdaVisitonce per request to a lambda or a preview (status, timing, bytes, path, domain, and the caller's country and agent fromCallerInfo) and only leaves it in the request's properties. Every reader takes it from there: the stats and overview figures (LambdaActivityConcern, now a reader that skips previews), the request log (CallerConcern, which no longer measures lambda requests itself), and the analytics. The overview and stats look exactly as before.TelemetryConcernstill counts every request to the server, the platform's included, so it has no visit to read.AnalyticsConcernreads the visit from the request and, only for lambdas with analytics on and never for previews, adds it toAnalyticsQueue(a lock-freeConcurrentQueuewith an interlocked counter, capped at 250k; anything dropped is logged).AnalyticsJobruns everyLAMBDA_ANALYTICS_INTERVAL_SECONDS(default 300). It takes what is queued, sums it into one row per lambda/day/value, and writes everything in one transaction. Referrer host, device, browser, system and crawler are worked out there, never on the request. TheRefererandSec-Fetch-Destheaders are only read for lambdas that count visits, and they are read before the body is bound.analytics.db, a star schema.lambdas, plus fact tablesdays,pages,referrers,countries,devices,browsers,systemsandcrawlers, each keyed(lambda, year, month, day[, value])andWITHOUT ROWID. Reads and retention use key ranges. It has its own Evolve migrations (Services/Analytics/Migrations) and uses plain synchronous commands. Only the switch lives inlambda.db(V21:lambdas.analytics_since), andResolvedLambdacarries it, so serving a request never asks a database.Sec-Fetch-Dest: document, including a 304), or a successfulGETthat returns HTML to a client that doesn't send that header. Paths are kept only for answered requests, capped at 200 per day (the rest go under(other)), so scanners can't fill a day. A referrer is stored as its host only, withoutwww., and only when it is another site.CrawlerJobdownloads a fresh one daily. Bots are counted by the list's kinds: search engine, AI crawler, link preview, SEO, monitoring, scanner, scripts…X-Forwarded-For, which anybody can set. Production terminates TLS itself, so that address is the real client; behind a proxy nothing would be confirmed.Sec-CH-UA*), because Chromium's user agent no longer says which phone or OS it runs on. Hints are only believed alongside a Chromium user agent, and a value is only taken when it is a known one, so nobody can write their own words into an owner's figures. In-app browsers (Instagram, Facebook, TikTok…) are named.SearchValues), the 25 real expressions run without backtracking, and at most 500 characters are read. Results are cached per distinct user agent.AnalyticsStore.Kept= 366 days). How much the owner sees is a new tier limit in the panel, Analytics shown: 7 days on free, 365 on premium. A lambda that becomes premium therefore sees its history.IAnalyticsStore.API:
GET / PUT / DELETE /api/v1/lambdas/:privateKey/analytics(?days=). MCP: a newanalyticstool that reads, and switches analytics on withenable: trueonly when the user asks.platform_guidehas ananalyticsentry telling agents not to build a counter of their own. Documented in the README (new Analytics section, API table, limits, configuration, services, database),/docs(Who visits it), the privacy page (new section about the visitors of lambdas), and CLAUDE.md. Translated into all 12 languages with the translator agents; the section's name is added toglossary.json.Decisions to confirm
These are mine and are recorded in CLAUDE.md. Please push back on any of them:
Switching off deletes what was counted, like switching off a kind of data. That is why agents can switch analytics on but never off.
It is in the simple view too. It is about the audience, not the code.
Daily unique visitors via an in-memory salted hash, people only. On an installation with
LAMBDA_LOG_CLIENT_ADDRESS=falsethere is no address to hash, so visitors stay at 0.The caller's country is now looked up even when addresses aren't logged. It names nobody and analytics need it. As a side effect, log lines on such installations now show a country.
The default range is 30 days (7 on free).
CLAUDE.md's PR-screenshot rule now describes the single
pr-assetsbranch, matching your earlier decision.Crawler data is downloaded at runtime. The server fetches the list and the five address lists daily (
LAMBDA_ANALYTICS_CRAWLER_LISTS=falseturns this off). Like the geo data, it only downloads; nothing about visitors is sent. The privacy page says so.Tests
VisitorTests: all 2,120 example agents that ship with the crawler list come out as bots; 17 real browser agents come out with the right browser, system and device (Cubot phones, in-app browsers and Chrome's frozen Android agent included); hints are only believed from Chromium and only with known values; a claimed Googlebot is named only from a published address, and never through a forwarding header; a broken download keeps the previous list; a 260 kB hostile agent stays cheap.AnalyticsTests(12 tests): off until switched on; off deletes; demos refused; page/referrer/country/device/browser/crawler classification over real requests; views vs. hits; the daily path cap; previews not counted; free shows 7 days and premium shows the history; one-year expiry; deleting a lambda deletes its rows; the MCP tool.LimitsTestsfor the new limit.dotnet test: 830 passed.npm run buildpasses.translations checkreports 0 errors.I ran the app locally (Kestrel, Vite) with live requests and 120 days of seeded history to make the screenshots.
🤖 Generated with Claude Code