Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 |
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/track-link-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
1 change: 1 addition & 0 deletions src/env-secrets.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
interface Env {
REDIRECTS?: AnalyticsEngineDataset;
ANALYTICS_API_TOKEN?: string;
API_KEY: string;
DEFAULT_SHORT_PATH_LENGTH?: string;
Expand Down
10 changes: 10 additions & 0 deletions test/analytics/track-link-redirect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
1 change: 0 additions & 1 deletion worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ declare namespace Cloudflare {
ACCOUNT_ID: "TODO";
API_TOKEN: string;
D1: D1Database;
REDIRECTS: AnalyticsEngineDataset;
}
}
interface Env extends Cloudflare.Env {}
Expand Down
4 changes: 0 additions & 4 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@
}
],

"analytics_engine_datasets": [
{ "binding": "REDIRECTS", "dataset": "ishere-redirects" }
],

"triggers": {
"crons": [ "0 * * * *" ]
}
Expand Down
Loading