Loading Gopkg.lock +28 −29 Original line number Diff line number Diff line Loading @@ -9,14 +9,14 @@ "monitoring/apiv3", "trace/apiv2" ] revision = "20d4028b8a750c2aca76bf9fefa8ed2d0109b573" version = "v0.19.0" revision = "4b98a6370e36d7a85192e7bad08a4ebd82eac2a8" version = "v0.20.0" [[projects]] branch = "master" name = "github.com/beorn7/perks" packages = ["quantile"] revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" revision = "3a771d992973f24aa725d07868b467d1ddfceafb" [[projects]] name = "github.com/davecgh/go-spew" Loading @@ -33,12 +33,13 @@ [[projects]] name = "github.com/go-yaml/yaml" packages = ["."] revision = "c95af922eae69f190717a0b7148960af8c55a072" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" [[projects]] name = "github.com/gobuffalo/packr" packages = ["."] revision = "6434a292ac52e6964adebfdce3f9ce6d9f16be01" revision = "3402a167bccf1bdd5c93ca820be9f27bba46261e" version = "v1.10.7" [[projects]] name = "github.com/golang/protobuf" Loading Loading @@ -89,7 +90,7 @@ [[projects]] name = "github.com/gorilla/websocket" packages = ["."] revision = "292fd08b2560ad524ee37396253d71570339a821" revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" [[projects]] name = "github.com/grpc-ecosystem/grpc-gateway" Loading @@ -108,7 +109,7 @@ ".", "oid" ] revision = "83612a56d3dd153a94a629cd64925371c9adad78" revision = "d34b9ff171c21ad295489235aec8b6626023cd04" [[projects]] name = "github.com/matttproud/golang_protobuf_extensions" Loading Loading @@ -150,7 +151,7 @@ "internal/bitbucket.org/ww/goautoneg", "model" ] revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0" revision = "38c53a9f4bfcd932d1b00bfc65e256a7fba6b37a" [[projects]] branch = "master" Loading @@ -161,7 +162,7 @@ "nfs", "xfs" ] revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349" revision = "780932d4fbbe0e69b84c34c20f5c8d0981e109ea" [[projects]] name = "github.com/rubenv/sql-migrate" Loading @@ -169,18 +170,18 @@ ".", "sqlparse" ] revision = "6edbfbd6a369ee82463312ec67b2c92f97a1d4dc" revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906" [[projects]] name = "github.com/satori/go.uuid" packages = ["."] revision = "063359185d32c6b045fa171ad7033ea545864fa1" revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" [[projects]] name = "github.com/stretchr/testify" packages = ["assert"] revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" [[projects]] name = "github.com/yuin/gopher-lua" Loading @@ -190,30 +191,28 @@ "parse", "pm" ] revision = "7d7bc8747e3f614c5c587729a341fe7d8903cdb8" revision = "84ea3a3c79b3c4b3b39606cc96f255cd63635ce0" [[projects]] name = "go.opencensus.io" packages = [ "exporter/prometheus", "exporter/stackdriver", "exporter/stackdriver/propagation", "internal", "internal/tagencoding", "plugin/ocgrpc", "plugin/ochttp", "plugin/ochttp/propagation/b3", "plugin/ochttp/propagation/google", "stats", "stats/internal", "stats/view", "tag", "trace", "trace/propagation", "zpages", "zpages/internal" "trace/propagation" ] revision = "983446b8dae3871316dc8610f7aa61e160b50b31" version = "v0.5.0" revision = "6e3f034057826b530038d93267906ec3c012183f" version = "v0.6.0" [[projects]] name = "go.uber.org/atomic" Loading Loading @@ -246,7 +245,7 @@ "bcrypt", "blowfish" ] revision = "1875d0a70c90e57f11972aefd42276df65e895b9" revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" [[projects]] branch = "master" Loading @@ -261,7 +260,7 @@ "lex/httplex", "trace" ] revision = "2fb46b16b8dda405028c50f7c7f0f9dd1fa6bfb1" revision = "6078986fec03a1dcc236c34816c71b0e05018fda" [[projects]] branch = "master" Loading @@ -282,7 +281,6 @@ revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" [[projects]] branch = "master" name = "golang.org/x/text" packages = [ "collate", Loading @@ -300,7 +298,8 @@ "unicode/norm", "unicode/rangetable" ] revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] branch = "master" Loading @@ -315,7 +314,7 @@ "transport/grpc", "transport/http" ] revision = "55e9fb4044f4757138d4273ace23060d022d18f9" revision = "dbbc13f71100fa6ece308335445fca6bb0dd5c2f" [[projects]] name = "google.golang.org/appengine" Loading Loading @@ -350,7 +349,7 @@ "googleapis/rpc/status", "protobuf/field_mask" ] revision = "4eb30f4778eed4c258ba66527a0d4f9ec8a36c45" revision = "ab0870e398d5dd054b868c0db1481ab029b9a9f2" [[projects]] name = "google.golang.org/grpc" Loading Loading @@ -381,8 +380,8 @@ "tap", "transport" ] revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" version = "v1.10.0" revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34" version = "v1.11.1" [[projects]] name = "gopkg.in/gorp.v1" Loading @@ -393,6 +392,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 inputs-digest = "d34f7dd83d8d4cdaa558556d02455ac16c463f0e61afdd76ecce2729e2486235" inputs-digest = "c9cf37873849b7c9ea30ef66151a1540afde6e0a518de051f8aef425e2e92f77" solver-name = "gps-cdcl" solver-version = 1 Gopkg.toml +10 −10 Original line number Diff line number Diff line Loading @@ -4,23 +4,23 @@ [[constraint]] name = "google.golang.org/grpc" version = "~1.10.0" version = "~1.11.1" [[constraint]] name = "github.com/go-yaml/yaml" revision = "c95af922eae69f190717a0b7148960af8c55a072" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" [[constraint]] name = "github.com/satori/go.uuid" revision = "063359185d32c6b045fa171ad7033ea545864fa1" revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" [[constraint]] name = "github.com/rubenv/sql-migrate" revision = "6edbfbd6a369ee82463312ec67b2c92f97a1d4dc" revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906" [[constraint]] name = "github.com/lib/pq" revision = "83612a56d3dd153a94a629cd64925371c9adad78" revision = "d34b9ff171c21ad295489235aec8b6626023cd04" [[constraint]] name = "github.com/gorilla/handlers" Loading @@ -36,11 +36,11 @@ [[constraint]] name = "github.com/gorilla/websocket" revision = "292fd08b2560ad524ee37396253d71570339a821" revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" [[constraint]] name = "github.com/yuin/gopher-lua" revision = "7d7bc8747e3f614c5c587729a341fe7d8903cdb8" revision = "84ea3a3c79b3c4b3b39606cc96f255cd63635ce0" [[constraint]] name = "github.com/gorhill/cronexpr" Loading @@ -48,11 +48,11 @@ [[constraint]] name = "golang.org/x/crypto" revision = "1875d0a70c90e57f11972aefd42276df65e895b9" revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" [[constraint]] name = "github.com/gobuffalo/packr" revision = "6434a292ac52e6964adebfdce3f9ce6d9f16be01" version = "~1.10.7" [[override]] name = "github.com/prometheus/client_golang" Loading @@ -60,4 +60,4 @@ [[constraint]] name = "github.com/stretchr/testify" version = "~1.1.4" version = "~1.2.1" ga/ga.go 0 → 100644 +117 −0 Original line number Diff line number Diff line // Copyright 2018 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ga import ( "bytes" "errors" "fmt" "net/http" "net/url" "regexp" ) const gaURL = "https://www.google-analytics.com/collect" var gacodeRegexp = regexp.MustCompile(`^UA-\d+-\d+$`) // Event is a GA event. type Event struct { Ec string // event category Ea string // event action El string // event label Ev string // event value } // AppInfo represents a mobile app info GA event. type AppInfo struct { An string // app name Aid string // app identifier Av string // app version Aiid string // app installer identifier } // SendAppInfo will send the AppInfo struct to GA over HTTP. func SendAppInfo(httpc *http.Client, gacode string, cookie string, app *AppInfo) error { values := url.Values{} values.Add("an", app.An) values.Add("av", app.Av) values.Add("aid", app.Aid) values.Add("aiid", app.Aiid) return SendValues(httpc, gacode, cookie, values) } // SendEvent will send the event struct to GA over HTTP. func SendEvent(httpc *http.Client, gacode string, cookie string, event *Event) error { if len(event.Ec) < 1 || len(event.Ea) < 1 { return errors.New("event category/action must be set") } values := url.Values{} values.Add("ec", event.Ec) values.Add("ea", event.Ea) if len(event.El) > 0 { values.Add("el", event.El) } if len(event.Ev) > 0 { values.Add("ev", event.Ev) } values.Add("t", "event") return SendValues(httpc, gacode, cookie, values) } // SendValues will send supplied values to GA over HTTP. func SendValues(httpc *http.Client, gacode string, cookie string, values url.Values) error { if !gacodeRegexp.MatchString(gacode) { return errors.New("invalid tracking id") } // Add required params values.Add("v", "1") values.Add("tid", gacode) values.Add("cid", cookie) // Send request buf := bytes.NewBufferString(values.Encode()) resp, err := httpc.Post(gaURL, "application/x-www-form-urlencoded", buf) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("request failed with '%v' status", resp.StatusCode) } return nil } // SendSessionStart will send a session start event to GA over HTTP. func SendSessionStart(httpc *http.Client, gacode string, cookie string) error { values := url.Values{} values.Add("t", "event") values.Add("sc", "start") return SendValues(httpc, gacode, cookie, values) } // SendSessionStop will send a session stop event to GA over HTTP. func SendSessionStop(httpc *http.Client, gacode string, cookie string) error { values := url.Values{} values.Add("t", "event") values.Add("sc", "end") return SendValues(httpc, gacode, cookie, values) } main.go +60 −3 Original line number Diff line number Diff line Loading @@ -28,13 +28,19 @@ import ( "time" "github.com/golang/protobuf/jsonpb" "github.com/heroiclabs/nakama/ga" "github.com/heroiclabs/nakama/migrate" "github.com/heroiclabs/nakama/server" "github.com/heroiclabs/nakama/social" _ "github.com/lib/pq" "github.com/satori/go.uuid" "go.uber.org/zap" "io/ioutil" "path/filepath" ) const cookieFilename = ".cookie" var ( version string = "2.0.0" commitID string = "dev" Loading Loading @@ -111,6 +117,13 @@ func main() { metrics := server.NewMetrics(multiLogger, config) apiServer := server.StartApiServer(jsonLogger, multiLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, sessionRegistry, matchRegistry, tracker, router, pipeline, runtimePool) gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1 cookie := newOrLoadCookie(config) gacode := "UA-89792135-1" if gaenabled { runTelemetry(jsonLogger, http.DefaultClient, gacode, cookie) } // Respect OS stop signals. c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) Loading @@ -128,6 +141,10 @@ func main() { tracker.Stop() sessionRegistry.Stop() if gaenabled { ga.SendSessionStop(http.DefaultClient, gacode, cookie) } os.Exit(0) } Loading Loading @@ -155,9 +172,6 @@ func dbConnect(multiLogger *zap.Logger, config server.Config) (*sql.DB, string) if err != nil { multiLogger.Fatal("Error pinging database", zap.Error(err)) } db.SetConnMaxLifetime(time.Millisecond * time.Duration(config.GetDatabase().ConnMaxLifetimeMs)) db.SetMaxOpenConns(config.GetDatabase().MaxOpenConns) db.SetMaxIdleConns(config.GetDatabase().MaxIdleConns) db.SetConnMaxLifetime(time.Millisecond * time.Duration(config.GetDatabase().ConnMaxLifetimeMs)) db.SetMaxOpenConns(config.GetDatabase().MaxOpenConns) Loading @@ -170,3 +184,46 @@ func dbConnect(multiLogger *zap.Logger, config server.Config) (*sql.DB, string) return db, dbVersion } // Help improve Nakama by sending anonymous usage statistics. // // You can disable the telemetry completely before server start by setting the // environment variable "NAKAMA_TELEMETRY" - i.e. NAKAMA_TELEMETRY=0 nakama // // These properties are collected: // * A unique UUID v4 random identifier which is generated. // * Version of Nakama being used which includes build metadata. // * Amount of time the server ran for. // // This information is sent via Google Analytics which allows the Nakama team to // analyze usage patterns and errors in order to help improve the server. func runTelemetry(logger *zap.Logger, httpc *http.Client, gacode string, cookie string) { err := ga.SendSessionStart(httpc, gacode, cookie) if err != nil { logger.Debug("Send start session event failed.", zap.Error(err)) return } err = ga.SendEvent(httpc, gacode, cookie, &ga.Event{Ec: "version", Ea: fmt.Sprintf("%s+%s", version, commitID)}) if err != nil { logger.Debug("Send event failed.", zap.Error(err)) return } err = ga.SendEvent(httpc, gacode, cookie, &ga.Event{Ec: "variant", Ea: "nakama"}) if err != nil { logger.Debug("Send event failed.", zap.Error(err)) return } } func newOrLoadCookie(config server.Config) string { filePath := filepath.FromSlash(config.GetDataDir() + "/" + cookieFilename) b, err := ioutil.ReadFile(filePath) cookie := uuid.FromBytesOrNil(b) if err != nil || cookie == uuid.Nil { cookie = uuid.Must(uuid.NewV4()) ioutil.WriteFile(filePath, cookie.Bytes(), 0644) } return cookie.String() } migrate/migrate-packr.go +1 −1 Original line number Diff line number Diff line Loading @@ -9,5 +9,5 @@ import "github.com/gobuffalo/packr" // Go binary. You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") } Loading
Gopkg.lock +28 −29 Original line number Diff line number Diff line Loading @@ -9,14 +9,14 @@ "monitoring/apiv3", "trace/apiv2" ] revision = "20d4028b8a750c2aca76bf9fefa8ed2d0109b573" version = "v0.19.0" revision = "4b98a6370e36d7a85192e7bad08a4ebd82eac2a8" version = "v0.20.0" [[projects]] branch = "master" name = "github.com/beorn7/perks" packages = ["quantile"] revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" revision = "3a771d992973f24aa725d07868b467d1ddfceafb" [[projects]] name = "github.com/davecgh/go-spew" Loading @@ -33,12 +33,13 @@ [[projects]] name = "github.com/go-yaml/yaml" packages = ["."] revision = "c95af922eae69f190717a0b7148960af8c55a072" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" [[projects]] name = "github.com/gobuffalo/packr" packages = ["."] revision = "6434a292ac52e6964adebfdce3f9ce6d9f16be01" revision = "3402a167bccf1bdd5c93ca820be9f27bba46261e" version = "v1.10.7" [[projects]] name = "github.com/golang/protobuf" Loading Loading @@ -89,7 +90,7 @@ [[projects]] name = "github.com/gorilla/websocket" packages = ["."] revision = "292fd08b2560ad524ee37396253d71570339a821" revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" [[projects]] name = "github.com/grpc-ecosystem/grpc-gateway" Loading @@ -108,7 +109,7 @@ ".", "oid" ] revision = "83612a56d3dd153a94a629cd64925371c9adad78" revision = "d34b9ff171c21ad295489235aec8b6626023cd04" [[projects]] name = "github.com/matttproud/golang_protobuf_extensions" Loading Loading @@ -150,7 +151,7 @@ "internal/bitbucket.org/ww/goautoneg", "model" ] revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0" revision = "38c53a9f4bfcd932d1b00bfc65e256a7fba6b37a" [[projects]] branch = "master" Loading @@ -161,7 +162,7 @@ "nfs", "xfs" ] revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349" revision = "780932d4fbbe0e69b84c34c20f5c8d0981e109ea" [[projects]] name = "github.com/rubenv/sql-migrate" Loading @@ -169,18 +170,18 @@ ".", "sqlparse" ] revision = "6edbfbd6a369ee82463312ec67b2c92f97a1d4dc" revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906" [[projects]] name = "github.com/satori/go.uuid" packages = ["."] revision = "063359185d32c6b045fa171ad7033ea545864fa1" revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" [[projects]] name = "github.com/stretchr/testify" packages = ["assert"] revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" [[projects]] name = "github.com/yuin/gopher-lua" Loading @@ -190,30 +191,28 @@ "parse", "pm" ] revision = "7d7bc8747e3f614c5c587729a341fe7d8903cdb8" revision = "84ea3a3c79b3c4b3b39606cc96f255cd63635ce0" [[projects]] name = "go.opencensus.io" packages = [ "exporter/prometheus", "exporter/stackdriver", "exporter/stackdriver/propagation", "internal", "internal/tagencoding", "plugin/ocgrpc", "plugin/ochttp", "plugin/ochttp/propagation/b3", "plugin/ochttp/propagation/google", "stats", "stats/internal", "stats/view", "tag", "trace", "trace/propagation", "zpages", "zpages/internal" "trace/propagation" ] revision = "983446b8dae3871316dc8610f7aa61e160b50b31" version = "v0.5.0" revision = "6e3f034057826b530038d93267906ec3c012183f" version = "v0.6.0" [[projects]] name = "go.uber.org/atomic" Loading Loading @@ -246,7 +245,7 @@ "bcrypt", "blowfish" ] revision = "1875d0a70c90e57f11972aefd42276df65e895b9" revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" [[projects]] branch = "master" Loading @@ -261,7 +260,7 @@ "lex/httplex", "trace" ] revision = "2fb46b16b8dda405028c50f7c7f0f9dd1fa6bfb1" revision = "6078986fec03a1dcc236c34816c71b0e05018fda" [[projects]] branch = "master" Loading @@ -282,7 +281,6 @@ revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" [[projects]] branch = "master" name = "golang.org/x/text" packages = [ "collate", Loading @@ -300,7 +298,8 @@ "unicode/norm", "unicode/rangetable" ] revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] branch = "master" Loading @@ -315,7 +314,7 @@ "transport/grpc", "transport/http" ] revision = "55e9fb4044f4757138d4273ace23060d022d18f9" revision = "dbbc13f71100fa6ece308335445fca6bb0dd5c2f" [[projects]] name = "google.golang.org/appengine" Loading Loading @@ -350,7 +349,7 @@ "googleapis/rpc/status", "protobuf/field_mask" ] revision = "4eb30f4778eed4c258ba66527a0d4f9ec8a36c45" revision = "ab0870e398d5dd054b868c0db1481ab029b9a9f2" [[projects]] name = "google.golang.org/grpc" Loading Loading @@ -381,8 +380,8 @@ "tap", "transport" ] revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" version = "v1.10.0" revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34" version = "v1.11.1" [[projects]] name = "gopkg.in/gorp.v1" Loading @@ -393,6 +392,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 inputs-digest = "d34f7dd83d8d4cdaa558556d02455ac16c463f0e61afdd76ecce2729e2486235" inputs-digest = "c9cf37873849b7c9ea30ef66151a1540afde6e0a518de051f8aef425e2e92f77" solver-name = "gps-cdcl" solver-version = 1
Gopkg.toml +10 −10 Original line number Diff line number Diff line Loading @@ -4,23 +4,23 @@ [[constraint]] name = "google.golang.org/grpc" version = "~1.10.0" version = "~1.11.1" [[constraint]] name = "github.com/go-yaml/yaml" revision = "c95af922eae69f190717a0b7148960af8c55a072" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" [[constraint]] name = "github.com/satori/go.uuid" revision = "063359185d32c6b045fa171ad7033ea545864fa1" revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" [[constraint]] name = "github.com/rubenv/sql-migrate" revision = "6edbfbd6a369ee82463312ec67b2c92f97a1d4dc" revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906" [[constraint]] name = "github.com/lib/pq" revision = "83612a56d3dd153a94a629cd64925371c9adad78" revision = "d34b9ff171c21ad295489235aec8b6626023cd04" [[constraint]] name = "github.com/gorilla/handlers" Loading @@ -36,11 +36,11 @@ [[constraint]] name = "github.com/gorilla/websocket" revision = "292fd08b2560ad524ee37396253d71570339a821" revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" [[constraint]] name = "github.com/yuin/gopher-lua" revision = "7d7bc8747e3f614c5c587729a341fe7d8903cdb8" revision = "84ea3a3c79b3c4b3b39606cc96f255cd63635ce0" [[constraint]] name = "github.com/gorhill/cronexpr" Loading @@ -48,11 +48,11 @@ [[constraint]] name = "golang.org/x/crypto" revision = "1875d0a70c90e57f11972aefd42276df65e895b9" revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" [[constraint]] name = "github.com/gobuffalo/packr" revision = "6434a292ac52e6964adebfdce3f9ce6d9f16be01" version = "~1.10.7" [[override]] name = "github.com/prometheus/client_golang" Loading @@ -60,4 +60,4 @@ [[constraint]] name = "github.com/stretchr/testify" version = "~1.1.4" version = "~1.2.1"
ga/ga.go 0 → 100644 +117 −0 Original line number Diff line number Diff line // Copyright 2018 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ga import ( "bytes" "errors" "fmt" "net/http" "net/url" "regexp" ) const gaURL = "https://www.google-analytics.com/collect" var gacodeRegexp = regexp.MustCompile(`^UA-\d+-\d+$`) // Event is a GA event. type Event struct { Ec string // event category Ea string // event action El string // event label Ev string // event value } // AppInfo represents a mobile app info GA event. type AppInfo struct { An string // app name Aid string // app identifier Av string // app version Aiid string // app installer identifier } // SendAppInfo will send the AppInfo struct to GA over HTTP. func SendAppInfo(httpc *http.Client, gacode string, cookie string, app *AppInfo) error { values := url.Values{} values.Add("an", app.An) values.Add("av", app.Av) values.Add("aid", app.Aid) values.Add("aiid", app.Aiid) return SendValues(httpc, gacode, cookie, values) } // SendEvent will send the event struct to GA over HTTP. func SendEvent(httpc *http.Client, gacode string, cookie string, event *Event) error { if len(event.Ec) < 1 || len(event.Ea) < 1 { return errors.New("event category/action must be set") } values := url.Values{} values.Add("ec", event.Ec) values.Add("ea", event.Ea) if len(event.El) > 0 { values.Add("el", event.El) } if len(event.Ev) > 0 { values.Add("ev", event.Ev) } values.Add("t", "event") return SendValues(httpc, gacode, cookie, values) } // SendValues will send supplied values to GA over HTTP. func SendValues(httpc *http.Client, gacode string, cookie string, values url.Values) error { if !gacodeRegexp.MatchString(gacode) { return errors.New("invalid tracking id") } // Add required params values.Add("v", "1") values.Add("tid", gacode) values.Add("cid", cookie) // Send request buf := bytes.NewBufferString(values.Encode()) resp, err := httpc.Post(gaURL, "application/x-www-form-urlencoded", buf) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("request failed with '%v' status", resp.StatusCode) } return nil } // SendSessionStart will send a session start event to GA over HTTP. func SendSessionStart(httpc *http.Client, gacode string, cookie string) error { values := url.Values{} values.Add("t", "event") values.Add("sc", "start") return SendValues(httpc, gacode, cookie, values) } // SendSessionStop will send a session stop event to GA over HTTP. func SendSessionStop(httpc *http.Client, gacode string, cookie string) error { values := url.Values{} values.Add("t", "event") values.Add("sc", "end") return SendValues(httpc, gacode, cookie, values) }
main.go +60 −3 Original line number Diff line number Diff line Loading @@ -28,13 +28,19 @@ import ( "time" "github.com/golang/protobuf/jsonpb" "github.com/heroiclabs/nakama/ga" "github.com/heroiclabs/nakama/migrate" "github.com/heroiclabs/nakama/server" "github.com/heroiclabs/nakama/social" _ "github.com/lib/pq" "github.com/satori/go.uuid" "go.uber.org/zap" "io/ioutil" "path/filepath" ) const cookieFilename = ".cookie" var ( version string = "2.0.0" commitID string = "dev" Loading Loading @@ -111,6 +117,13 @@ func main() { metrics := server.NewMetrics(multiLogger, config) apiServer := server.StartApiServer(jsonLogger, multiLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, sessionRegistry, matchRegistry, tracker, router, pipeline, runtimePool) gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1 cookie := newOrLoadCookie(config) gacode := "UA-89792135-1" if gaenabled { runTelemetry(jsonLogger, http.DefaultClient, gacode, cookie) } // Respect OS stop signals. c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) Loading @@ -128,6 +141,10 @@ func main() { tracker.Stop() sessionRegistry.Stop() if gaenabled { ga.SendSessionStop(http.DefaultClient, gacode, cookie) } os.Exit(0) } Loading Loading @@ -155,9 +172,6 @@ func dbConnect(multiLogger *zap.Logger, config server.Config) (*sql.DB, string) if err != nil { multiLogger.Fatal("Error pinging database", zap.Error(err)) } db.SetConnMaxLifetime(time.Millisecond * time.Duration(config.GetDatabase().ConnMaxLifetimeMs)) db.SetMaxOpenConns(config.GetDatabase().MaxOpenConns) db.SetMaxIdleConns(config.GetDatabase().MaxIdleConns) db.SetConnMaxLifetime(time.Millisecond * time.Duration(config.GetDatabase().ConnMaxLifetimeMs)) db.SetMaxOpenConns(config.GetDatabase().MaxOpenConns) Loading @@ -170,3 +184,46 @@ func dbConnect(multiLogger *zap.Logger, config server.Config) (*sql.DB, string) return db, dbVersion } // Help improve Nakama by sending anonymous usage statistics. // // You can disable the telemetry completely before server start by setting the // environment variable "NAKAMA_TELEMETRY" - i.e. NAKAMA_TELEMETRY=0 nakama // // These properties are collected: // * A unique UUID v4 random identifier which is generated. // * Version of Nakama being used which includes build metadata. // * Amount of time the server ran for. // // This information is sent via Google Analytics which allows the Nakama team to // analyze usage patterns and errors in order to help improve the server. func runTelemetry(logger *zap.Logger, httpc *http.Client, gacode string, cookie string) { err := ga.SendSessionStart(httpc, gacode, cookie) if err != nil { logger.Debug("Send start session event failed.", zap.Error(err)) return } err = ga.SendEvent(httpc, gacode, cookie, &ga.Event{Ec: "version", Ea: fmt.Sprintf("%s+%s", version, commitID)}) if err != nil { logger.Debug("Send event failed.", zap.Error(err)) return } err = ga.SendEvent(httpc, gacode, cookie, &ga.Event{Ec: "variant", Ea: "nakama"}) if err != nil { logger.Debug("Send event failed.", zap.Error(err)) return } } func newOrLoadCookie(config server.Config) string { filePath := filepath.FromSlash(config.GetDataDir() + "/" + cookieFilename) b, err := ioutil.ReadFile(filePath) cookie := uuid.FromBytesOrNil(b) if err != nil || cookie == uuid.Nil { cookie = uuid.Must(uuid.NewV4()) ioutil.WriteFile(filePath, cookie.Bytes(), 0644) } return cookie.String() }
migrate/migrate-packr.go +1 −1 Original line number Diff line number Diff line Loading @@ -9,5 +9,5 @@ import "github.com/gobuffalo/packr" // Go binary. You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") }