Create ga-proxy
This commit is contained in:
parent
5eb506adbd
commit
8ba6b097b0
|
@ -0,0 +1,42 @@
|
|||
name: "ga-proxy docker build"
|
||||
|
||||
env:
|
||||
PROJECT: ga-proxy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set tag
|
||||
id: tag
|
||||
run: |
|
||||
TAG=$(cat ${{ env.PROJECT }}/Dockerfile | awk 'NR==4 {print $3}')
|
||||
echo "::set-env name=TAG::$TAG"
|
||||
- name: Docker Hub login
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
buildx-version: latest
|
||||
- name: Build Dockerfile
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--output "type=image,push=true" \
|
||||
--file ${{ env.PROJECT }}/Dockerfile ./${{ env.PROJECT }} \
|
||||
--tag $(echo "${DOCKER_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ env.PROJECT }}:latest \
|
||||
--tag $(echo "${DOCKER_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ env.PROJECT }}:${TAG}
|
|
@ -0,0 +1,5 @@
|
|||
node_module/
|
||||
js/
|
||||
package.json
|
||||
|
||||
*.log
|
|
@ -0,0 +1,17 @@
|
|||
# build dir
|
||||
build/
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
|
@ -0,0 +1,25 @@
|
|||
matrix:
|
||||
include:
|
||||
- language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
script: make
|
||||
cache:
|
||||
directories:
|
||||
- $GOPATH/pkg/mod
|
||||
- $HOME/.cache/go-build
|
||||
- language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
before_install: cd packages/ga
|
||||
install: yarn install
|
||||
script:
|
||||
- yarn test
|
||||
- yarn build
|
||||
- yarn size
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- packages/ga/node_modules
|
|
@ -0,0 +1,32 @@
|
|||
FROM golang:1.11-alpine AS BUILD
|
||||
|
||||
|
||||
ENV VERSION 1.2.0
|
||||
WORKDIR /src
|
||||
|
||||
# module
|
||||
RUN apk --no-cache add git ca-certificates tzdata && update-ca-certificates
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# build
|
||||
COPY ga ga
|
||||
COPY server server
|
||||
COPY main.go ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -o ga-proxy
|
||||
|
||||
FROM alpine:3.8
|
||||
LABEL maintainer "giuem <giuemcom+docker@gmail.com>"
|
||||
EXPOSE 80
|
||||
ENV IP=0.0.0.0
|
||||
ENV PORT=80
|
||||
ENV GIN_MODE=release
|
||||
|
||||
COPY --from=BUILD /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=BUILD /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=BUILD /src/ga-proxy /ga-proxy
|
||||
|
||||
HEALTHCHECK --interval=3m --timeout=10s --start-period=2s --retries=3 \
|
||||
CMD /ga-proxy ping
|
||||
|
||||
CMD ["/ga-proxy"]
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Giuem
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,20 @@
|
|||
.PHONY: build clean test
|
||||
|
||||
GOCMD=go
|
||||
GOBUILD=$(GOCMD) build
|
||||
GOCLEAN=$(GOCMD) clean
|
||||
GOTEST=$(GOCMD) test
|
||||
BINARY_NAME=ga-proxy
|
||||
OUTPUT_DIR=build
|
||||
|
||||
all: test build
|
||||
|
||||
build:
|
||||
$(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME) -v
|
||||
|
||||
clean:
|
||||
$(GOCLEAN)
|
||||
rm -rf $(OUTPUT_DIR)
|
||||
|
||||
test:
|
||||
$(GOTEST) -v ./...
|
|
@ -0,0 +1,56 @@
|
|||
# ga-proxy
|
||||
|
||||
[![Travis Status](https://img.shields.io/travis/com/giuem/ga-proxy.svg?style=flat-square)](https://travis-ci.com/giuem/ga-proxy)
|
||||
[![Docker Build Status](https://img.shields.io/docker/build/giuem/ga-proxy.svg?style=flat-square)](https://hub.docker.com/r/giuem/ga-proxy/)
|
||||
[![GitHub release](https://img.shields.io/github/release/giuem/ga-proxy.svg?style=flat-square)](https://github.com/giuem/ga-proxy/releases/latest)
|
||||
[![Size](https://img.badgesize.io/https://unpkg.com/@giuem/ga-proxy/dist/ga.min.js?compression=gzip&style=flat-square)](https://unpkg.com/@giuem/ga-proxy/dist/)
|
||||
[![](https://data.jsdelivr.com/v1/package/npm/@giuem/ga-proxy/badge)](https://www.jsdelivr.com/package/npm/@giuem/ga-proxy)
|
||||
|
||||
Accelerate Google Analytics.
|
||||
|
||||
## Get Start
|
||||
|
||||
### Run via Docker
|
||||
|
||||
```bash
|
||||
docker pull giuem/ga-proxy
|
||||
docker run -d -p <port>:80 --name <container_name> giuem/ga-proxy
|
||||
```
|
||||
|
||||
### Run as you like
|
||||
|
||||
#### 1. Install
|
||||
|
||||
Download binary from [release](https://github.com/giuem/ga-proxy/releases) or build yourself.
|
||||
|
||||
#### 2. Run
|
||||
|
||||
```
|
||||
GIN_MODE=release ./ga-proxy [arguments]
|
||||
```
|
||||
|
||||
options:
|
||||
|
||||
```
|
||||
--ip IP, -i IP IP to listen (default: "127.0.0.1") [$IP]
|
||||
--port port, -p port port to listen (default: "9080") [$PORT]
|
||||
```
|
||||
|
||||
e.g.
|
||||
|
||||
```
|
||||
./ga_proxy -i 0.0.0.0 -p 80
|
||||
```
|
||||
|
||||
### 3. Insert script to your website
|
||||
|
||||
``` html
|
||||
<script>
|
||||
// replace following variables to your own
|
||||
window.ga_tid = "UA-XXXXX-Y";
|
||||
window.ga_url = "https://ga.giuem.com";
|
||||
</script>
|
||||
<script src="https://unpkg.com/@giuem/ga-proxy/dist/ga.min.js" async></script>
|
||||
```
|
||||
|
||||
Note: `ga.giuem.com` is my own service, it do not promise any SLA and may shutdown at some day. You'd better deploy your own server.
|
|
@ -0,0 +1,8 @@
|
|||
version: "3.3"
|
||||
services:
|
||||
proxy:
|
||||
image: giuem/ga-proxy
|
||||
restart: always
|
||||
ports:
|
||||
- 9080:80
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const gaURL = "https://www.google-analytics.com/collect"
|
||||
|
||||
var httpClient = &http.Client{}
|
||||
|
||||
func send(qs string) error {
|
||||
req, err := http.NewRequest(http.MethodPost, gaURL, strings.NewReader(qs))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create request")
|
||||
}
|
||||
|
||||
// https://golang.org/pkg/net/http/#Client.Do
|
||||
// On error, any Response can be ignored. A non-nil Response with a non-nil error only occurs when
|
||||
// CheckRedirect fails, and even then the returned Response.Body is already closed.
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not make request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func concatURLValues(v1 url.Values, v2 url.Values) {
|
||||
for key, values := range v2 {
|
||||
if len(v2.Get(key)) != 0 && len(v1.Get(key)) == 0 {
|
||||
// do not replace existed key
|
||||
v1[key] = values
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConcatURLValues(t *testing.T) {
|
||||
v1 := make(url.Values)
|
||||
v2 := make(url.Values)
|
||||
|
||||
v1.Set("a", "1")
|
||||
v1.Set("b", "2")
|
||||
v2.Set("a", "2")
|
||||
v2.Set("c", "3")
|
||||
|
||||
concatURLValues(v1, v2)
|
||||
|
||||
if v1.Encode() != "a=1&b=2&c=3" {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if v2.Encode() != "a=2&c=3" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PageView sends analysis data of t=pageview
|
||||
func PageView(data CommonData) error {
|
||||
data.HitType = "pageview"
|
||||
|
||||
v, err := query.Values(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not encode query")
|
||||
}
|
||||
|
||||
err = send(v.Encode())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timing sends analysis data of t=timing
|
||||
func Timing(data CommonData, tData TimingData) error {
|
||||
data.HitType = "timing"
|
||||
|
||||
v1, err := query.Values(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not encode query")
|
||||
}
|
||||
v2, err := query.Values(tData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not encode query")
|
||||
}
|
||||
concatURLValues(v1, v2)
|
||||
|
||||
err = send(v1.Encode())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detect tests network connection
|
||||
func Detect() error {
|
||||
err := send("")
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package ga
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
err := Detect()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package ga
|
||||
|
||||
// CommonData includes all necessary data
|
||||
type CommonData struct {
|
||||
// general
|
||||
Version int `url:"v"`
|
||||
TrackingID string `url:"tid"`
|
||||
|
||||
// user
|
||||
ClientID string `url:"cid"`
|
||||
|
||||
// t
|
||||
HitType string `url:"t"`
|
||||
|
||||
// session
|
||||
UserIP string `url:"uip"`
|
||||
UserAgent string `url:"ua"`
|
||||
|
||||
// trafficsources
|
||||
DocumentReferer string `url:"dr,omitempty"`
|
||||
|
||||
// system
|
||||
ScreenResolution string `url:"sr,omitempty"`
|
||||
ViewportSize string `url:"vp,omitempty"`
|
||||
DocumentEncoding string `url:"de,omitempty"`
|
||||
ScreenColors string `url:"sd,omitempty"`
|
||||
UserLanguage string `url:"ul,omitempty"`
|
||||
|
||||
// content
|
||||
DocumentLink string `url:"dl"`
|
||||
DocumentTitle string `url:"dt,omitempty"`
|
||||
}
|
||||
|
||||
// TimingData contains all fields of `HitType=timing`
|
||||
type TimingData struct {
|
||||
PageLoadedTime string `url:"plt,omitempty"`
|
||||
DNSTime string `url:"dns,omitempty"`
|
||||
PageDownloadedTime string `url:"pdt,omitempty"`
|
||||
RedirectTime string `url:"rrt,omitempty"`
|
||||
TCPTime string `url:"tcp,omitempty"`
|
||||
ServerResponseTime string `url:"srt,omitempty"`
|
||||
DomInteractiveTime string `url:"dit,omitempty"`
|
||||
ContentLoadedTime string `url:"clt,omitempty"`
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
module github.com/giuem/ga-proxy
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 // indirect
|
||||
github.com/gin-gonic/gin v1.3.0
|
||||
github.com/gofrs/uuid v3.1.0+incompatible
|
||||
github.com/golang/protobuf v1.2.0 // indirect
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/json-iterator/go v1.1.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 // indirect
|
||||
github.com/urfave/cli v1.20.0
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 h1:FaI7wNyesdMBSkIRVUuEEYEvmzufs7EqQvRAxfEXGbQ=
|
||||
github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
|
||||
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
|
||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/giuem/ga-proxy/server"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "ga-proxy"
|
||||
app.HideVersion = true
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "ip, i",
|
||||
Value: "127.0.0.1",
|
||||
Usage: "`IP` to listen",
|
||||
EnvVar: "IP",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "port, p",
|
||||
Value: "9080",
|
||||
Usage: "`port` to listen",
|
||||
EnvVar: "PORT",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) error {
|
||||
server.Run(c.String("ip"), c.String("port"))
|
||||
return nil
|
||||
}
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
cli.Command{
|
||||
Name: "ping",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "ip, i",
|
||||
Value: "127.0.0.1",
|
||||
Usage: "server `IP`",
|
||||
EnvVar: "IP",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "port, p",
|
||||
Value: "9080",
|
||||
Usage: "server `port`",
|
||||
EnvVar: "PORT",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
resp, err := http.Get(fmt.Sprintf("http://%v:%v/ping", c.String("ip"), c.String("port")))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return cli.NewExitError(fmt.Errorf("server returns non-200 status code"), 1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
js/dist
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
extends: ["plugin:prettier/recommended"],
|
||||
rules: {
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module"
|
||||
}
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
|
||||
# Created by https://www.gitignore.io/api/node
|
||||
# Edit at https://www.gitignore.io/?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# End of https://www.gitignore.io/api/node
|
|
@ -0,0 +1,93 @@
|
|||
(function(win, doc, navigator) {
|
||||
var screen = win.screen;
|
||||
var encode = encodeURIComponent;
|
||||
var max = Math.max;
|
||||
// const min = Math.min;
|
||||
var performance = win.performance;
|
||||
var timing = performance && performance.timing;
|
||||
var navigation = performance && performance.navigation;
|
||||
|
||||
var pvData = {
|
||||
dt: doc.title,
|
||||
de: doc.characterSet || doc.charset,
|
||||
dr: doc.referrer || void 0,
|
||||
ul:
|
||||
navigator.language ||
|
||||
navigator.browserLanguage ||
|
||||
navigator.userLanguage ||
|
||||
void 0,
|
||||
sd: screen.colorDepth + "-bit",
|
||||
sr: screen.width + "x" + screen.height,
|
||||
vp:
|
||||
max(doc.documentElement.clientWidth, win.innerWidth || 0) +
|
||||
"x" +
|
||||
max(doc.documentElement.clientHeight, win.innerHeight || 0),
|
||||
ga: win.ga_tid,
|
||||
z: new Date().getTime()
|
||||
};
|
||||
|
||||
function buildQueryString(params) {
|
||||
var qs = [];
|
||||
for (var k in params) {
|
||||
if (params.hasOwnProperty(k) && params[k] !== void 0) {
|
||||
qs.push(encode(k) + "=" + encode(params[k]));
|
||||
}
|
||||
}
|
||||
return qs.join("&");
|
||||
}
|
||||
|
||||
function sendViaImg(uri, params) {
|
||||
var img = new Image();
|
||||
// img.width = img.height = 1;
|
||||
img.referrerPolicy = "unsafe-url";
|
||||
img.src = uri + "?" + buildQueryString(params);
|
||||
}
|
||||
|
||||
// function sendBeacon(uri, params) {
|
||||
// if (!navigator.sendBeacon) return false;
|
||||
// return navigator.sendBeacon(uri, params);
|
||||
// }
|
||||
|
||||
function send(uri, params) {
|
||||
uri = win.ga_url + uri;
|
||||
// if (!sendBeacon(uri, params)) {
|
||||
sendViaImg(uri, params);
|
||||
// }
|
||||
}
|
||||
|
||||
function sendTiming() {
|
||||
if (!timing) { return; }
|
||||
var navigationStart = timing.navigationStart;
|
||||
if (navigationStart == 0) { return; }
|
||||
|
||||
var filterNumber = function (num) { return isNaN(num) || num == Infinity || num < 0 ? void 0 : num; };
|
||||
|
||||
var perfData = {
|
||||
plt: filterNumber(timing.loadEventStart - navigationStart),
|
||||
dns: filterNumber(timing.domainLookupEnd - timing.domainLookupStart),
|
||||
pdt: filterNumber(timing.responseEnd - timing.responseStart),
|
||||
rrt: filterNumber(timing.redirectEnd - timing.redirectStart),
|
||||
tcp: filterNumber(timing.connectEnd - timing.connectStart),
|
||||
srt: filterNumber(timing.responseStart - timing.requestStart),
|
||||
dit: filterNumber(timing.domInteractive - navigationStart),
|
||||
clt: filterNumber(timing.domContentLoadedEventStart - navigationStart)
|
||||
};
|
||||
|
||||
for (var key in pvData) {
|
||||
perfData[key] = pvData[key];
|
||||
}
|
||||
|
||||
send("/t", perfData);
|
||||
}
|
||||
|
||||
if (!navigation || navigation.type != navigation.TYPE_RELOAD) {
|
||||
// page view
|
||||
send("/p", pvData);
|
||||
// timing
|
||||
if (document.readyState == "complete") {
|
||||
sendTiming();
|
||||
} else {
|
||||
win.addEventListener("load", sendTiming);
|
||||
}
|
||||
}
|
||||
})(window, document, navigator);
|
|
@ -0,0 +1 @@
|
|||
!function(n,t,e){var r=n.screen,a=encodeURIComponent,i=Math.max,o=n.performance,d=o&&o.timing,c=o&&o.navigation,u={dt:t.title,de:t.characterSet||t.charset,dr:t.referrer||void 0,ul:e.language||e.browserLanguage||e.userLanguage||void 0,sd:r.colorDepth+"-bit",sr:r.width+"x"+r.height,vp:i(t.documentElement.clientWidth,n.innerWidth||0)+"x"+i(t.documentElement.clientHeight,n.innerHeight||0),ga:n.ga_tid,z:(new Date).getTime()};function s(t,e){var n=new Image;n.referrerPolicy="unsafe-url",n.src=t+"?"+function r(t){var e=[];for(var n in t)t.hasOwnProperty(n)&&void 0!==t[n]&&e.push(a(n)+"="+a(t[n]));return e.join("&")}(e)}function v(t,e){s(t=n.ga_url+t,e)}function g(){if(d){var t=d.navigationStart;if(0!=t){var e=function(t){return isNaN(t)||t==Infinity||t<0?void 0:t},n={plt:e(d.loadEventStart-t),dns:e(d.domainLookupEnd-d.domainLookupStart),pdt:e(d.responseEnd-d.responseStart),rrt:e(d.redirectEnd-d.redirectStart),tcp:e(d.connectEnd-d.connectStart),srt:e(d.responseStart-d.requestStart),dit:e(d.domInteractive-t),clt:e(d.domContentLoadedEventStart-t)};for(var r in u)n[r]=u[r];v("/t",n)}}}c&&c.type==c.TYPE_RELOAD||(v("/p",u),"complete"==document.readyState?g():n.addEventListener("load",g))}(window,document,navigator);
|
|
@ -0,0 +1,21 @@
|
|||
const gulp = require("gulp");
|
||||
const buble = require("gulp-buble");
|
||||
const uglify = require("gulp-uglify");
|
||||
const rename = require("gulp-rename");
|
||||
|
||||
function build() {
|
||||
return gulp
|
||||
.src("./src/**/*.js")
|
||||
.pipe(buble())
|
||||
.pipe(gulp.dest("./dist"))
|
||||
.pipe(
|
||||
uglify({
|
||||
mangle: true,
|
||||
ie8: true
|
||||
})
|
||||
)
|
||||
.pipe(rename({ suffix: ".min" }))
|
||||
.pipe(gulp.dest("./dist"));
|
||||
}
|
||||
|
||||
exports.build = build;
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "@giuem/ga-proxy",
|
||||
"version": "1.2.0",
|
||||
"main": "dist/ga.min.js",
|
||||
"repository": "git@github.com:giuem/ga-proxy.git",
|
||||
"author": "giuem <giuemcom@gmail.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"test": "npm run lint",
|
||||
"lint": "eslint ./src/**/*.js",
|
||||
"size": "bundlesize"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"bundlesize": "^0.17.1",
|
||||
"eslint": "^5.13.0",
|
||||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-buble": "^0.9.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-uglify": "^3.0.1",
|
||||
"prettier": "^1.16.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"gulp-buble/buble": "0.19.6"
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/*.min.js",
|
||||
"maxSize": "1kb"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
(function(win, doc, navigator) {
|
||||
const screen = win.screen;
|
||||
const encode = encodeURIComponent;
|
||||
const max = Math.max;
|
||||
// const min = Math.min;
|
||||
const performance = win.performance;
|
||||
const timing = performance && performance.timing;
|
||||
const navigation = performance && performance.navigation;
|
||||
|
||||
const pvData = {
|
||||
dt: doc.title,
|
||||
de: doc.characterSet || doc.charset,
|
||||
dr: doc.referrer || void 0,
|
||||
ul:
|
||||
navigator.language ||
|
||||
navigator.browserLanguage ||
|
||||
navigator.userLanguage ||
|
||||
void 0,
|
||||
sd: screen.colorDepth + "-bit",
|
||||
sr: screen.width + "x" + screen.height,
|
||||
vp:
|
||||
max(doc.documentElement.clientWidth, win.innerWidth || 0) +
|
||||
"x" +
|
||||
max(doc.documentElement.clientHeight, win.innerHeight || 0),
|
||||
ga: win.ga_tid,
|
||||
z: new Date().getTime()
|
||||
};
|
||||
|
||||
function buildQueryString(params) {
|
||||
const qs = [];
|
||||
for (const k in params) {
|
||||
if (params.hasOwnProperty(k) && params[k] !== void 0) {
|
||||
qs.push(encode(k) + "=" + encode(params[k]));
|
||||
}
|
||||
}
|
||||
return qs.join("&");
|
||||
}
|
||||
|
||||
function sendViaImg(uri, params) {
|
||||
const img = new Image();
|
||||
// img.width = img.height = 1;
|
||||
img.referrerPolicy = "unsafe-url";
|
||||
img.src = uri + "?" + buildQueryString(params);
|
||||
}
|
||||
|
||||
// function sendBeacon(uri, params) {
|
||||
// if (!navigator.sendBeacon) return false;
|
||||
// return navigator.sendBeacon(uri, params);
|
||||
// }
|
||||
|
||||
function send(uri, params) {
|
||||
uri = win.ga_url + uri;
|
||||
// if (!sendBeacon(uri, params)) {
|
||||
sendViaImg(uri, params);
|
||||
// }
|
||||
}
|
||||
|
||||
function sendTiming() {
|
||||
if (!timing) return;
|
||||
const navigationStart = timing.navigationStart;
|
||||
if (navigationStart == 0) return;
|
||||
|
||||
const filterNumber = num =>
|
||||
isNaN(num) || num == Infinity || num < 0 ? void 0 : num;
|
||||
|
||||
const perfData = {
|
||||
plt: filterNumber(timing.loadEventStart - navigationStart),
|
||||
dns: filterNumber(timing.domainLookupEnd - timing.domainLookupStart),
|
||||
pdt: filterNumber(timing.responseEnd - timing.responseStart),
|
||||
rrt: filterNumber(timing.redirectEnd - timing.redirectStart),
|
||||
tcp: filterNumber(timing.connectEnd - timing.connectStart),
|
||||
srt: filterNumber(timing.responseStart - timing.requestStart),
|
||||
dit: filterNumber(timing.domInteractive - navigationStart),
|
||||
clt: filterNumber(timing.domContentLoadedEventStart - navigationStart)
|
||||
};
|
||||
|
||||
for (const key in pvData) {
|
||||
perfData[key] = pvData[key];
|
||||
}
|
||||
|
||||
send("/t", perfData);
|
||||
}
|
||||
|
||||
if (!navigation || navigation.type != navigation.TYPE_RELOAD) {
|
||||
// page view
|
||||
send("/p", pvData);
|
||||
// timing
|
||||
if (document.readyState == "complete") {
|
||||
sendTiming();
|
||||
} else {
|
||||
win.addEventListener("load", sendTiming);
|
||||
}
|
||||
}
|
||||
})(window, document, navigator);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,66 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/giuem/ga-proxy/ga"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func getUUID(c *gin.Context) string {
|
||||
uid, err := c.Cookie("uuid")
|
||||
if err != nil { // cookie no found
|
||||
uid = generateUUID(c.Request.UserAgent())
|
||||
c.SetCookie("uuid", uid, 2147483647, "/", "", false, false)
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
func generateUUID(name string) string {
|
||||
ns, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
// error fallback
|
||||
unix32bits := uint32(time.Now().UTC().Unix())
|
||||
nameBytes := md5.Sum([]byte(name))
|
||||
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x\n", unix32bits, nameBytes[0:2], nameBytes[2:4], nameBytes[4:6], nameBytes[6:12])
|
||||
}
|
||||
|
||||
return uuid.NewV5(ns, name).String()
|
||||
}
|
||||
|
||||
func getCommonData(c *gin.Context) ga.CommonData {
|
||||
return ga.CommonData{
|
||||
Version: 1,
|
||||
TrackingID: c.Query("ga"),
|
||||
ClientID: getUUID(c),
|
||||
UserIP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
DocumentReferer: c.Query("dr"),
|
||||
ScreenResolution: c.Query("sr"),
|
||||
ViewportSize: c.Query("vp"),
|
||||
DocumentEncoding: c.Query("de"),
|
||||
ScreenColors: c.Query("sd"),
|
||||
UserLanguage: c.Query("ul"),
|
||||
DocumentLink: c.Request.Referer(),
|
||||
DocumentTitle: c.Query("dt"),
|
||||
}
|
||||
}
|
||||
|
||||
func getTimingData(c *gin.Context) ga.TimingData {
|
||||
return ga.TimingData{
|
||||
PageLoadedTime: c.Query("plt"),
|
||||
DNSTime: c.Query("dns"),
|
||||
PageDownloadedTime: c.Query("pdt"),
|
||||
RedirectTime: c.Query("rrt"),
|
||||
TCPTime: c.Query("tcp"),
|
||||
ServerResponseTime: c.Query("srt"),
|
||||
DomInteractiveTime: c.Query("dit"),
|
||||
ContentLoadedTime: c.Query("clt"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/giuem/ga-proxy/ga"
|
||||
)
|
||||
|
||||
func handlePageView(c *gin.Context) {
|
||||
if len(c.Request.Referer()) == 0 || len(c.Query("ga")) == 0 {
|
||||
handleRedirect(c)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
||||
go ga.PageView(getCommonData(c))
|
||||
}
|
||||
|
||||
func handleTiming(c *gin.Context) {
|
||||
if len(c.Request.Referer()) == 0 || len(c.Query("ga")) == 0 {
|
||||
handleRedirect(c)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
||||
go ga.Timing(getCommonData(c), getTimingData(c))
|
||||
}
|
||||
|
||||
func handlePing(c *gin.Context) {
|
||||
err := ga.Detect()
|
||||
if err != nil {
|
||||
if c.Request.Method == http.MethodHead {
|
||||
c.Status(http.StatusBadGateway)
|
||||
} else {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"msg": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if c.Request.Method == http.MethodHead {
|
||||
c.Status(http.StatusOK)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleRedirect(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "https://github.com/giuem/ga-proxy")
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Run starts a HTTP server
|
||||
func Run(ip, port string) {
|
||||
addr := fmt.Sprintf("%v:%v", ip, port)
|
||||
|
||||
r := gin.New()
|
||||
logger := gin.Logger()
|
||||
r.Use(func(c *gin.Context) {
|
||||
if c.Request.URL.Path == "/ping" {
|
||||
return
|
||||
}
|
||||
logger(c)
|
||||
})
|
||||
|
||||
r.NoRoute(handleRedirect)
|
||||
// version < 1
|
||||
r.GET("/", handlePageView)
|
||||
// version >= 1
|
||||
r.GET("/p", handlePageView)
|
||||
r.GET("/t", handleTiming)
|
||||
|
||||
r.GET("/ping", handlePing)
|
||||
r.HEAD("/ping", handlePing)
|
||||
|
||||
r.Run(addr)
|
||||
}
|
Loading…
Reference in New Issue