diff --git a/README.md b/README.md index a714719..aed209c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A fast, edge-deployed link shortening service built with [Hono](https://hono.dev - **Custom short paths** — define your own paths or let them auto-generate (`/abc12`, `/your-brand/campaign`) - **Namespaces** — organize links under a namespace prefix - **QR codes** — append `/qr` to any short link for SVG, PNG, or HTML output -- **Analytics** — per-link redirect stats with bot detection, grouped by hour or day +- **Analytics** (optional, requires [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/)) — per-link redirect stats with bot detection, grouped by hour or day - **Password protection** — optionally require a password before redirecting - **Link scheduling** — set a go-live date so the link only activates at a specific time - **Link expiration** — optional TTL-based expiry with automatic cleanup @@ -22,7 +22,7 @@ A fast, edge-deployed link shortening service built with [Hono](https://hono.dev ### One-Click Deploy -Click the button above to deploy to Cloudflare. The deploy flow will automatically create KV, D1, and Analytics Engine resources and prompt you for secrets. +Click the button above to deploy to Cloudflare. The deploy flow will automatically create KV and D1 resources and prompt you for secrets. Analytics is optional — see [Enabling Analytics](#enabling-analytics) below. ### Local Development @@ -126,8 +126,8 @@ curl https://your-domain/api/link/abc12/stats/day \ | Variable | Required | Description | | -------------------------- | -------- | -------------------------------------------------------- | | `API_KEY` | Yes | Secret key for authenticating API requests | -| `ANALYTICS_API_TOKEN` | Yes | Cloudflare API token for querying Analytics Engine | -| `ACCOUNT_ID` | Yes | Your Cloudflare Account ID | +| `ANALYTICS_API_TOKEN` | No | Cloudflare API token for querying Analytics Engine (see [Enabling Analytics](#enabling-analytics)) | +| `ACCOUNT_ID` | No | Your Cloudflare Account ID (required for analytics) | | `DEFAULT_SHORT_PATH_LENGTH`| No | Length of auto-generated short paths (default: `5`) | | `MAX_SHORT_ID_RETRIES` | No | Max retries on ID collision (default: `5`) | | `SLACK_BOT_TOKEN` | No | Slack Bot User OAuth Token (`xoxb-...`) for notifications | @@ -138,6 +138,22 @@ curl https://your-domain/api/link/abc12/stats/day \ Set secrets locally in `.dev.vars` and via `wrangler secret put` for deployed environments. +## Enabling Analytics + +Analytics requires the [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) ($5/mo) for Analytics Engine access. To enable: + +1. Add the Analytics Engine binding to `wrangler.jsonc`: + +```jsonc +"analytics_engine_datasets": [ + { "binding": "REDIRECTS", "dataset": "ishere-redirects" } +], +``` + +2. Set the `ACCOUNT_ID` and `ANALYTICS_API_TOKEN` secrets (via `wrangler secret put` or `.dev.vars`). + +Without these, the link shortener works normally — analytics tracking and the stats endpoint are simply disabled. + ## Slack App Setup The Slack integration lets you create, look up, and manage short links via a slash command and global shortcuts, with optional bot notifications when links are created or updated. @@ -225,7 +241,7 @@ Routes (src/routes/) → Actions (src/actions/) → KV (src/kv/) + D1 (src/db/) - **Actions** contain business logic, decoupled from HTTP concerns - **D1** is the source of truth; **KV** serves as a global edge cache for fast reads - Reads try KV first, falling back to D1 on cache miss — this means links are available immediately after creation, avoiding the ~60s propagation delay that KV-only link shorteners suffer from -- Analytics are tracked via Cloudflare Analytics Engine on each redirect +- Analytics are optionally tracked via Cloudflare Analytics Engine on each redirect (requires Workers Paid plan) - A cron trigger runs hourly to clean up expired links ## Testing diff --git a/src/analytics/track-link-redirect.ts b/src/analytics/track-link-redirect.ts index 93545f3..6281022 100644 --- a/src/analytics/track-link-redirect.ts +++ b/src/analytics/track-link-redirect.ts @@ -23,6 +23,8 @@ const parseCoordinate = (coord: string | undefined): number => coord ? parseFloat(coord) : 0; const trackLinkRedirect = async (id: string, { cf: cfProps, headers }: Request, env: Env) => { + if (!env.REDIRECTS) return; + const cf = cfProps || {}; const userAgent = headers.get('user-agent') || 'unknown'; diff --git a/src/env-secrets.d.ts b/src/env-secrets.d.ts index f7d5a8a..337e7ac 100644 --- a/src/env-secrets.d.ts +++ b/src/env-secrets.d.ts @@ -1,4 +1,5 @@ interface Env { + REDIRECTS?: AnalyticsEngineDataset; ANALYTICS_API_TOKEN?: string; API_KEY: string; DEFAULT_SHORT_PATH_LENGTH?: string; diff --git a/test/analytics/track-link-redirect.spec.ts b/test/analytics/track-link-redirect.spec.ts index ed265b3..84c2a67 100644 --- a/test/analytics/track-link-redirect.spec.ts +++ b/test/analytics/track-link-redirect.spec.ts @@ -74,6 +74,16 @@ describe('trackLinkRedirect', () => { expect(args.blobs[0]).toBe('unknown'); }); + it('should silently skip tracking when REDIRECTS binding is not configured', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const env = { } as unknown as Env; + + await trackLinkRedirect('no-analytics', makeRequest(), env); + + expect(consoleSpy).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + it('should catch and log write errors', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const env = { diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index db0d996..361f1e3 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -8,7 +8,6 @@ declare namespace Cloudflare { ACCOUNT_ID: "TODO"; API_TOKEN: string; D1: D1Database; - REDIRECTS: AnalyticsEngineDataset; } } interface Env extends Cloudflare.Env {} diff --git a/wrangler.jsonc b/wrangler.jsonc index 4a24e41..6afcf74 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -30,10 +30,6 @@ } ], - "analytics_engine_datasets": [ - { "binding": "REDIRECTS", "dataset": "ishere-redirects" } - ], - "triggers": { "crons": [ "0 * * * *" ] }