Create ga-proxy

This commit is contained in:
Stille 2021-06-08 20:46:29 +08:00
parent 5eb506adbd
commit 8ba6b097b0
29 changed files with 4707 additions and 0 deletions

42
.github/workflows/ga-proxy.yml vendored Normal file
View File

@ -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}

5
ga-proxy/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_module/
js/
package.json
*.log

17
ga-proxy/.gitignore vendored Normal file
View File

@ -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/

25
ga-proxy/.travis.yml Normal file
View File

@ -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

32
ga-proxy/Dockerfile Normal file
View File

@ -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"]

21
ga-proxy/LICENSE Normal file
View File

@ -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.

20
ga-proxy/Makefile Normal file
View File

@ -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 ./...

56
ga-proxy/README.md Normal file
View File

@ -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.

View File

@ -0,0 +1,8 @@
version: "3.3"
services:
proxy:
image: giuem/ga-proxy
restart: always
ports:
- 9080:80

40
ga-proxy/ga/client.go Normal file
View File

@ -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
}
}
}

View File

@ -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()
}
}

45
ga-proxy/ga/ga.go Normal file
View File

@ -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
}

10
ga-proxy/ga/ga_test.go Normal file
View File

@ -0,0 +1,10 @@
package ga
import "testing"
func TestDetect(t *testing.T) {
err := Detect()
if err != nil {
t.Error(err)
}
}

44
ga-proxy/ga/types.go Normal file
View File

@ -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"`
}

19
ga-proxy/go.mod Normal file
View File

@ -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
)

39
ga-proxy/go.sum Normal file
View File

@ -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=

68
ga-proxy/main.go Normal file
View File

@ -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)
}

View File

@ -0,0 +1 @@
js/dist

View File

@ -0,0 +1,13 @@
module.exports = {
extends: ["plugin:prettier/recommended"],
rules: {
"prettier/prettier": "error",
},
env: {
browser: true,
es6: true
},
parserOptions: {
sourceType: "module"
}
};

87
ga-proxy/packages/ga/.gitignore vendored Normal file
View File

@ -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

93
ga-proxy/packages/ga/dist/ga.js vendored Normal file
View File

@ -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);

1
ga-proxy/packages/ga/dist/ga.min.js vendored Normal file
View File

@ -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);

View File

@ -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;

View File

@ -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"
}
]
}

View File

@ -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

66
ga-proxy/server/helper.go Normal file
View File

@ -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"),
}
}

52
ga-proxy/server/router.go Normal file
View File

@ -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")
}

33
ga-proxy/server/server.go Normal file
View File

@ -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)
}