base on Scalable web analytics you run yourself on Cloudflare # Counterscale ![](/packages/server/public/counterscale-logo-300x300.webp) ![ci status](https://github.com/benvinegar/counterscale/actions/workflows/ci.yaml/badge.svg) [![License](https://img.shields.io/github/license/benvinegar/counterscale)](https://github.com/benvinegar/counterscale/blob/master/LICENSE) [![codecov](https://codecov.io/gh/benvinegar/counterscale/graph/badge.svg?token=NUHURNB682)](https://codecov.io/gh/benvinegar/counterscale) Counterscale is a simple web analytics tracker and dashboard that you self-host on Cloudflare. It's designed to be easy to deploy and maintain, and should cost you near-zero to operate – even at high levels of traffic (Cloudflare's [free tier](https://developers.cloudflare.com/workers/platform/pricing/#workers) could hypothetically support up to 100k hits/day). _Counterscale is sponsored by [Modem, your dev-team's auto-triage PM](https://modem.dev)._ ## License Counterscale is free, open source software made available under the MIT license. See: [LICENSE](LICENSE). ## Limitations Counterscale is powered primarily by Cloudflare Workers and [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/). As of February 2025, Workers Analytics Engine has _maximum 90 days retention_, which means Counterscale can only show the last 90 days of recorded data. We do, however, provide long term storage of your data in an R2 bucket using Apache Arrow files. This long term storage is enabled by default and can be disabled using the CLI. ## Installation ### Requirements * macOS or Linux environment * Node v20 or above * An active [Cloudflare](https://cloudflare.com) account (either free or paid) ### Cloudflare Preparation If you don't have one already, [create a Cloudflare account here](https://dash.cloudflare.com/sign-up) and verify your email address. 1. Go to your Cloudflare dashboard and, if you do not already have one, set up a [Cloudflare Workers subdomain](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/) 1. Enable [Cloudflare Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) beta for your account. To enable, navigate to Storage & Databases > Analytics Engine and click the "Enable" button ([screenshot](./docs/enable-analytics-engine.png)). You can ignore and exit out of the "Create Dataset" menu that will pop up next. - Note: If this is your first time using Workers, you have to create a Worker before you can enable the Analytics Engine. Navigate to Workers & Pages > Overview, click the "Create Worker" button ([screenshot](./docs/create-worker.png)) to create a "Hello World" worker (it doesn't matter what you name this Worker as you can delete it later). 1. Create a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/). This token needs `Account.Account Analytics` permissions at a minimum ([screenshot](./docs/api-token.png)). - _WARNING: Keep this window open or copy your API token somewhere safe (e.g. a password manager), because if you close this window you will not be able to access this API token again and have to start over._ ### Deploy Counterscale First, sign into Cloudflare and authorize the Cloudflare CLI (Wrangler) using: ```bash npx wrangler login ``` Afterwards, run the Counterscale installer: ```bash npx @counterscale/cli@latest install ``` Follow the prompts. You will be asked for the Cloudflare API token you created earlier. You'll also be asked if you want to protect your dashboard with a password: - If you choose **Yes** (recommended for public deployments), you'll be prompted to create a password that will be required to access your analytics dashboard. - If you choose **No**, your dashboard will be publicly accessible without authentication. Once the script has finished, the server application should be deployed. Visit `https://{subdomain-emitted-during-deploy}.workers.dev` to verify. NOTE: _If this is your first time deploying Counterscale, it may take take a few minutes before the Worker subdomain becomes live._ ### Start Recording Web Traffic from Your Website(s) You can load the tracking code using one of two methods: #### 1. Script Loader (CDN) When Counterscale is deployed, it makes `tracker.js` available at the URL you deployed to: ``` https://{subdomain-emitted-during-deploy}.workers.dev/tracker.js ``` To start reporting website traffic from your web property, copy/paste the following snippet into your website HTML: ```html <script id="counterscale-script" data-site-id="your-unique-site-id" src="https://{subdomain-emitted-during-deploy}.workers.dev/tracker.js" defer ></script> ``` #### 2. Package/Module The Counterscale tracker is published as an npm module: ```bash npm install @counterscale/tracker ``` Initialize Counterscale with your site ID and the URL of your deployed reporting endpoint: ```typescript import * as Counterscale from "@counterscale/tracker"; Counterscale.init({ siteId: "your-unique-site-id", reporterUrl: "https://{subdomain-emitted-during-deploy}.workers.dev/collect", }); ``` **Available Methods** | Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | `init(opts)` | `ClientOpts` | `void` | Initializes the Counterscale client with site configuration. Creates a global client instance if one doesn't exist. | | `isInitialized()` | None | `boolean` | Checks if the Counterscale client has been initialized. Returns true if client exists, false otherwise. | | `getInitializedClient()` | None | `Client \| undefined` | Returns the initialized client instance or undefined if not initialized. | | `trackPageview(opts?)` | `TrackPageviewOpts?` | `void` | Tracks a pageview event. Requires client to be initialized first. Automatically detects URL and referrer if not provided. | | `cleanup()` | None | `void` | Cleans up the client instance and removes event listeners. Sets global client to undefined. | #### 3. Server-Side Module If you'd prefer to track analytics on the server, instead of running the tracker in the browser, use the `/server` module: ```bash npm install @counterscale/tracker ``` ```typescript import * as Counterscale from "@counterscale/tracker/server"; // Initialize the tracker Counterscale.init({ siteId: "your-unique-site-id", reporterUrl: "https://{subdomain-emitted-during-deploy}.workers.dev/collect", reportOnLocalhost: false, // optional, defaults to false timeout: 2000, // optional, defaults to 1000ms }); // Track a pageview await Counterscale.trackPageview({ url: "https://example.com/page", // or relative: '/page' hostname: "example.com", // required for relative URLs referrer: "https://google.com", utmSource: "social", utmMedium: "twitter", }); ``` **Server Module Methods** | Method | Parameters | Return Type | Description | |--------|------------|-------------|-------------| | `init(opts)` | `ServerClientOpts` | `void` | Initializes the server-side tracker. | | `isInitialized()` | None | `boolean` | Checks if the tracker has been initialized. | | `getInitializedClient()` | None | `ServerClient \| undefined` | Returns the initialized server client instance. | | `trackPageview(opts)` | `TrackPageviewOpts` | `Promise<void>` | Tracks a pageview event. Requires explicit URL and hostname parameters. | | `cleanup()` | None | `void` | Cleans up the server client instance. | The server module is designed for backend applications and differs from the client-side version: - No DOM-dependent features (auto-tracking, browser instrumentation) - Uses fetch API instead of XMLHttpRequest - Requires explicit URL and hostname parameters - Fire-and-forget - tracking errors won't throw exceptions ## Upgrading For most releases, upgrading is as simple as re-running the CLI installer: ```bash npx @counterscale/cli@latest install # OR # npx @counterscale/cli@VERSION install ``` You won't have to enter a new API key, and your data will carry forrward. Counterscale uses [semantic versioning](https://semver.org/). If upgrading to a major version (e.g. 2.x, 3.x, 4.x), there may be extra steps. Please consult the [release notes](https://github.com/benvinegar/counterscale/releases). ## Troubleshooting If the website is not immediately available (e.g. "Secure Connection Failed"), it could be because Cloudflare has not yet activated your subdomain (yoursubdomain.workers.dev). This process can take a minute; you can check in on the progress by visiting the newly created worker in your Cloudflare dashboard (Workers & Pages → counterscale). ## Advanced ### Manually Track Pageviews When you initialize the Counterscale tracker, set `autoTrackPageviews` to `false`. Then, you can manually call `Counterscale.trackPageview()` when you want to record a pageview. ```typescript import * as Counterscale from "@counterscale/tracker"; Counterscale.init({ siteId: "your-unique-site-id", reporterUrl: "https://{subdomain-emitted-during-deploy}.workers.dev/collect", autoTrackPageviews: false, // <- don't forget this }); // ... when a pageview happens Counterscale.trackPageview(); ``` ### Custom Domains The deployment URL can always be changed to go behind a custom domain you own. [More here](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/). ## CLI Commands Counterscale provides a command-line interface (CLI) to help you install, configure, and manage your deployment. ### Available Commands #### `install` The main command for installing and deploying Counterscale to Cloudflare. ```bash npx @counterscale/cli@latest install ``` Options: - `--advanced` - Enable advanced mode to customize worker name and analytics dataset - `--verbose` - Show additional logging information #### `auth` Manage authentication settings for your Counterscale deployment. ```bash npx @counterscale/cli@latest auth [subcommand] ``` Available subcommands: - `enable` - Enable authentication for your Counterscale deployment - `disable` - Disable authentication for your Counterscale deployment - `roll` - Update/roll the authentication password ##### Examples: Enable authentication: ```bash npx @counterscale/cli@latest auth enable ``` Disable authentication: ```bash npx @counterscale/cli@latest auth disable ``` Update/roll the password: ```bash npx @counterscale/cli@latest auth roll ``` #### `storage` Manage long term storage settings for your Counterscale deployment. ```bash npx @counterscale/cli@latest storage [subcommand] ``` Available subcommands: - `enable` - Enable storage for your Counterscale deployment - `disable` - Disable storage for your Counterscale deployment ##### Examples: Enable storage: ```bash npx @counterscale/cli@latest storage enable ``` Disable storage: ```bash npx @counterscale/cli@latest storage disable ``` ## Development See [Contributing](CONTRIBUTING.md) for information on how to get started. ## Notes ### Database There is only one "database": the Cloudflare Analytics Engine dataset, which is communicated entirely over HTTP using Cloudflare's API. Right now there is no local "test" database. This means in local development: - Writes will no-op (no hits will be recorded) - Reads will be read from the production Analaytics Engine dataset (local development shows production data) ### Sampling Cloudflare Analytics Engine uses sampling to make high volume data ingestion/querying affordable at scale (this is similar to most other analytics tools, see [Google Analytics on Sampling](https://support.google.com/analytics/answer/2637192?hl=en#zippy=%2Cin-this-article)). You can find out more how [sampling works with CF AE here](https://developers.cloudflare.com/analytics/analytics-engine/sampling/). ", Assign "at most 3 tags" to the expected json: {"id":"7467","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"