Skip to content

Commit f95d1a2

Browse files
committed
Add Rybbit analytics tracking
1 parent 33e7a75 commit f95d1a2

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A minimal, self-hosted URL shortener written in Go using bbolt (BoltDB). This pr
99
- List stored URLs with cursor-based pagination
1010
- Simple token-based authorization
1111
- Single-file embedded database (bbolt)
12+
- Tracking analytics with Rybbit (other providers soon to come)
1213

1314
## Quick Start
1415

@@ -44,6 +45,9 @@ Environment variables:
4445
- `APP_TOKEN` (required): token used for authorization on protected endpoints
4546
- `PORT` (optional): HTTP port (default `8080`)
4647
- `DB_PATH` (optional): path to BoltDB file (default `short-it.db`)
48+
- `RYBBIT_SITE_ID` (optional): Site ID provided by Rybbit
49+
- `RYBBIT_SITE_KEY` (optional): API key found in account settings for Rybbit
50+
- `RYBBIT_SITE_URL` (optional): Base URL for your Rybbit instance
4751

4852
## HTTP API
4953

@@ -111,4 +115,4 @@ Key implementation files:
111115

112116
- The app uses a BoltDB bucket named `urls` to store key → URL mappings.
113117
- The autogenerated keys are 6 characters drawn from `a-zA-Z0-9`.
114-
118+
- Sends pageview events to Rybbit if configured with the hostname, language, pathname, user-agent, referrer, and IP address gathered from `X-Forwarded-For`.

cmd/short-it/main.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package main
22

33
import (
4+
"bytes"
5+
"context"
46
"crypto/rand"
57
"encoding/json"
68
"fmt"
9+
"io"
710
"log"
811
"net/http"
912
"os"
1013
"strconv"
1114
"strings"
15+
"time"
1216

1317
"go.etcd.io/bbolt"
1418
)
1519

1620
var db *bbolt.DB
1721
var authToken string
1822

23+
var rybbitSiteID = os.Getenv("RYBBIT_SITE_ID")
24+
var rybbitSiteKey = os.Getenv("RYBBIT_SITE_KEY")
25+
var rybbitSiteURL = os.Getenv("RYBBIT_SITE_URL")
26+
var rybbitClient = &http.Client{Timeout: 3 * time.Second}
27+
1928
const bucketName = "urls"
2029
const maxPageSize = 100
2130

@@ -216,6 +225,7 @@ func handleGetURL(w http.ResponseWriter, r *http.Request) {
216225
return
217226
}
218227

228+
handlePageView(r)
219229
http.Redirect(w, r, url, http.StatusFound)
220230
}
221231

@@ -269,6 +279,65 @@ func handleDeleteURL(w http.ResponseWriter, r *http.Request, path string) {
269279
w.WriteHeader(http.StatusNoContent)
270280
}
271281

282+
func handlePageView(r *http.Request) {
283+
if rybbitSiteID == "" || rybbitSiteKey == "" || rybbitSiteURL == "" {
284+
return
285+
}
286+
287+
ip := r.Header.Get("X-Forwarded-For")
288+
if ip == "" {
289+
ip = strings.Split(r.RemoteAddr, ":")[0]
290+
} else {
291+
ip = strings.TrimSpace(strings.Split(ip, ",")[0])
292+
}
293+
294+
hostname := r.Host
295+
if hostname == "" {
296+
if h := r.URL.Hostname(); h != "" {
297+
hostname = h
298+
}
299+
}
300+
301+
data := map[string]string{
302+
"site_id": rybbitSiteID,
303+
"type": "pageview",
304+
"pathname": r.URL.Path,
305+
"hostname": hostname,
306+
"referrer": r.Referer(),
307+
"language": r.Header.Get("Accept-Language"),
308+
"user_agent": r.UserAgent(),
309+
"ip_address": ip,
310+
}
311+
312+
jsonData, err := json.Marshal(data)
313+
if err != nil {
314+
return
315+
}
316+
317+
go func(body []byte) {
318+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
319+
defer cancel()
320+
321+
req, err := http.NewRequestWithContext(ctx, "POST", strings.TrimRight(rybbitSiteURL, "/")+"/api/track", bytes.NewReader(body))
322+
if err != nil {
323+
return
324+
}
325+
req.Header.Set("Content-Type", "application/json")
326+
req.Header.Set("Authorization", "Bearer "+rybbitSiteKey)
327+
328+
resp, err := rybbitClient.Do(req)
329+
if err != nil {
330+
return
331+
}
332+
defer resp.Body.Close()
333+
334+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
335+
b, _ := io.ReadAll(resp.Body)
336+
log.Printf("Rybbit tracking error: %s", string(b))
337+
}
338+
}(jsonData)
339+
}
340+
272341
func main() {
273342
authToken = os.Getenv("APP_TOKEN")
274343
if authToken == "" {

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ services:
66
ports:
77
- "8080:8080"
88
environment:
9+
#- RYBBIT_SITE_ID=YOUR_SITE_ID_HERE
10+
#- RYBBIT_SITE_KEY=YOUR_API_KEY_HERE
11+
#- RYBBIT_SITE_URL=https://your-rybbit-instance.example.com
912
- APP_TOKEN=YOUR_TOKEN_HERE
1013
- PORT=8080
1114
- DB_PATH=/data/short-it.db

0 commit comments

Comments
 (0)