crowne/server sends events from a server, such as a Cloudflare Worker or Node, for actions that happen there, such as a payment, or for pages that run no script. It also serves the first-party proxy.
npm install crowneIt needs a key with the events:write scope. Make one on the org's Keys page in the app, and store it as a secret, such as CROWNE_KEY. Never send the key to a browser.
import { env } from "cloudflare:workers";
import { crowne } from "crowne/server";
let analytics = crowne({ key: env.CROWNE_KEY, site: "YOUR_SITE_ID" });
export default {
async fetch(request, env, ctx) {
ctx.waitUntil(analytics.pageview(request));
return new Response("Hello");
},
};crowne(options)
| Option | Use |
|---|---|
key |
A key with the events:write scope, crw_live_…. Required. |
site |
The id of the site the events count for. Required. |
endpoint |
Where events go. Default: https://in.crowne.io/v1/events. |
fetch |
The fetch function to send with. Default: the global fetch. |
ip |
Reads the visitor's address from a request. See Visitor address. |
pageview(request)
Counts the page that request asks for, as the visitor who sent it. It returns a promise; hand it to waitUntil, so the response does not wait for it.
track(request, name, props?)
Counts a custom event of the visitor behind request.
ctx.waitUntil(
analytics.track(request, "purchase", { plan: "pro", amount: 49 }),
);pageview and track reject with the status and the reason when crowne refuses the event, such as for a bad key.
proxy(request)
Answers the two paths of a first-party proxy:
GETof a path that ends in.jsanswers crowne's script, cached for one hour.POSTforwards the browser's events as it sent them, with the key and the visitor in theCrowne-Visitorheader, and without cookies. It answers crowne's answer.- Other methods answer
405.
Visitor address
crowne tells visitors apart by their address, so the package must know it, and the visitor must not be able to choose it.
On Cloudflare Workers, the package reads CF-Connecting-IP. Cloudflare sets this header, and a visitor cannot fake it. The package knows that it runs on Workers from navigator.userAgent, which needs the global_navigator compatibility flag. The flag is on by default from compatibility date 2022-03-21. If your Worker uses an older date or the no_global_navigator flag, add global_navigator to compatibility_flags, or set the ip option.
Everywhere else, the package trusts no header, because a visitor can send any header, including CF-Connecting-IP and X-Forwarded-For. Set the ip option to read the address from a source you trust. Without an address, crowne still counts each page view and event, but as an anonymous visitor: it cannot tell apart visitors with the same browser, so it counts them as one. The package warns once when this happens.
| Where the server runs | ip |
|---|---|
| Cloudflare Workers | Not needed |
| Behind Cloudflare, with the origin open only to Cloudflare | (request) => request.headers.get("cf-connecting-ip") |
| Vercel | (request) => request.headers.get("x-vercel-forwarded-for") |
| Fly.io | (request) => request.headers.get("fly-client-ip") |
| Bun | (request) => server.requestIP(request)?.address, as below |
| Deno | The handler's info.remoteAddr.hostname, as below |
| Node | req.socket.remoteAddress, as below |
Read a host's own header only on that host. On any other host, the visitor can send it. Behind your own proxy, such as nginx, read the header that the proxy sets new on each request. Never use the first X-Forwarded-For entry, because the visitor controls it.
Bun
server.requestIP gives the address of a request.
import { crowne } from "crowne/server";
let analytics = crowne({
key: process.env.CROWNE_KEY,
site: "YOUR_SITE_ID",
ip: (request) => server.requestIP(request)?.address,
});
let server = Bun.serve({
fetch(request) {
analytics.pageview(request).catch(console.error);
return new Response("Hello");
},
});Deno
Deno gives the address to the handler, not to the request. Make a client in the handler; it costs little.
import { crowne } from "npm:crowne/server";
let options = { key: Deno.env.get("CROWNE_KEY"), site: "YOUR_SITE_ID" };
Deno.serve((request, info) => {
let analytics = crowne({ ...options, ip: () => info.remoteAddr.hostname });
analytics.pageview(request).catch(console.error);
return new Response("Hello");
});Node
Node gives the address on the socket of each incoming message. It is the visitor's address only when no proxy is in front of the server. Behind a proxy, use the header that the proxy sets.
import { createServer } from "node:http";
import { crowne } from "crowne/server";
let options = { key: process.env.CROWNE_KEY, site: "YOUR_SITE_ID" };
createServer((req, res) => {
let request = new Request(`http://${req.headers.host}${req.url}`, {
headers: req.headers,
});
let analytics = crowne({ ...options, ip: () => req.socket.remoteAddress });
analytics.pageview(request).catch(console.error);
res.end("Hello");
}).listen(3000);The visitor
Besides the address, the package reads the browser from User-Agent, the language from Accept-Language, and the place from Cloudflare's request.cf. Outside Cloudflare Workers, events have no place. To send one, use the REST API.