Skip to content
Permalink
master
Go to file
 
 
Cannot retrieve contributors at this time
188 lines (163 sloc) 5.16 KB
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"google.golang.org/appengine/delay"
)
const beaconURL = "http://www.google-analytics.com/collect"
var (
pixel = mustReadFile("static/pixel.gif")
badge = mustReadFile("static/badge.svg")
badgeGif = mustReadFile("static/badge.gif")
badgeFlat = mustReadFile("static/badge-flat.svg")
badgeFlatGif = mustReadFile("static/badge-flat.gif")
pageTemplate = template.Must(template.New("page").ParseFiles("page.html"))
)
func main() {
http.HandleFunc("/", handler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func mustReadFile(path string) []byte {
b, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
return b
}
func generateUUID(cid *string) error {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return err
}
b[8] = (b[8] | 0x80) & 0xBF // what's the purpose ?
b[6] = (b[6] | 0x40) & 0x4F // what's the purpose ?
*cid = hex.EncodeToString(b)
return nil
}
var delayHit = delay.Func("collect", logHit)
func sendToGA(c context.Context, ua string, ip string, cid string, values url.Values) error {
client := &http.Client{}
req, _ := http.NewRequest("POST", beaconURL, strings.NewReader(values.Encode()))
req.Header.Add("User-Agent", ua)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if resp, err := client.Do(req); err != nil {
log.Printf("GA collector POST error: %s", err.Error())
return err
} else {
log.Printf("GA collector status: %v, cid: %v, ip: %s", resp.Status, cid, ip)
log.Printf("Reported payload: %v", values)
}
return nil
}
func logHit(c context.Context, params []string, query url.Values, ua string, ip string, cid string) error {
// 1) Initialize default values from path structure
// 2) Allow query param override to report arbitrary values to GA
//
// GA Protocol reference: https://developers.google.com/analytics/devguides/collection/protocol/v1/reference
payload := url.Values{
"v": {"1"}, // protocol version = 1
"t": {"pageview"}, // hit type
"tid": {params[0]}, // tracking / property ID
"cid": {cid}, // unique client ID (server generated UUID)
"dp": {params[1]}, // page path
"uip": {ip}, // IP address of the user
}
for key, val := range query {
payload[key] = val
}
return sendToGA(c, ua, ip, cid, payload)
}
func handler(w http.ResponseWriter, r *http.Request) {
c := r.Context()
params := strings.SplitN(strings.Trim(r.URL.Path, "/"), "/", 2)
query, _ := url.ParseQuery(r.URL.RawQuery)
refOrg := r.Header.Get("Referer")
// / -> redirect
if len(params[0]) == 0 {
http.Redirect(w, r, "https://github.com/igrigorik/ga-beacon", http.StatusFound)
return
}
// activate referrer path if ?useReferer is used and if referer exists
if _, ok := query["useReferer"]; ok {
if len(refOrg) != 0 {
referer := strings.Replace(strings.Replace(refOrg, "http://", "", 1), "https://", "", 1)
if len(referer) != 0 {
// if the useReferer is present and the referer information exists
// the path is ignored and the beacon referer information is used instead.
params = strings.SplitN(strings.Trim(r.URL.Path, "/")+"/"+referer, "/", 2)
}
}
}
// /account -> account template
if len(params) == 1 {
templateParams := struct {
Account string
Referer string
}{
Account: params[0],
Referer: refOrg,
}
if err := pageTemplate.ExecuteTemplate(w, "page.html", templateParams); err != nil {
http.Error(w, "could not show account page", 500)
log.Printf("Cannot execute template: %v", err)
}
return
}
// /account/page -> GIF + log pageview to GA collector
var cid string
if cookie, err := r.Cookie("cid"); err != nil {
if err := generateUUID(&cid); err != nil {
log.Printf("Failed to generate client UUID: %v", err)
} else {
log.Printf("Generated new client UUID: %v", cid)
http.SetCookie(w, &http.Cookie{Name: "cid", Value: cid, Path: fmt.Sprint("/", params[0])})
}
} else {
cid = cookie.Value
log.Printf("Existing CID found: %v", cid)
}
if len(cid) != 0 {
var cacheUntil = time.Now().Format(http.TimeFormat)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, private")
w.Header().Set("Expires", cacheUntil)
w.Header().Set("CID", cid)
logHit(c, params, query, r.Header.Get("User-Agent"), r.RemoteAddr, cid)
// delayHit.Call(c, params, r.Header.Get("User-Agent"), cid)
}
// Write out GIF pixel or badge, based on presence of "pixel" param.
if _, ok := query["pixel"]; ok {
w.Header().Set("Content-Type", "image/gif")
w.Write(pixel)
} else if _, ok := query["gif"]; ok {
w.Header().Set("Content-Type", "image/gif")
w.Write(badgeGif)
} else if _, ok := query["flat"]; ok {
w.Header().Set("Content-Type", "image/svg+xml")
w.Write(badgeFlat)
} else if _, ok := query["flat-gif"]; ok {
w.Header().Set("Content-Type", "image/gif")
w.Write(badgeFlatGif)
} else {
w.Header().Set("Content-Type", "image/svg+xml")
w.Write(badge)
}
}
You can’t perform that action at this time.