crownedocs

Server package

Count page views and events from your server, or serve a first-party proxy, with crowne/server.

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.

Shell
npm install crowne

It 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.

JavaScript
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.

JavaScript
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:

  • GET of a path that ends in .js answers crowne's script, cached for one hour.
  • POST forwards the browser's events as it sent them, with the key and the visitor in the Crowne-Visitor header, 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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

    Type to search page titles and sections.