From 11e7c3f9d694cee48749f0d0d2881b884aa057cd Mon Sep 17 00:00:00 2001 From: Shahidh K Muhammed Date: Mon, 28 Jan 2019 19:25:28 +0530 Subject: [PATCH] add anonymous telemetry (#1401) --- cli/Gopkg.lock | 141 +++++++++-- cli/assets/assets.go | 12 +- cli/assets/unversioned/console.html | 4 +- cli/assets/v1.0-alpha/console.html | 4 +- cli/assets/v1.0/console.html | 4 +- cli/cli.go | 204 ++++++++++++++-- cli/cli_test.go | 16 +- cli/commands/console.go | 19 +- cli/commands/console_test.go | 26 +- cli/commands/docs.go | 5 +- cli/commands/init.go | 2 + cli/commands/init_test.go | 17 +- cli/commands/root.go | 18 +- cli/commands/version.go | 2 +- cli/get.sh | 117 +++++++++ cli/telemetry/telemetry.go | 120 ++++++++++ cli/telemetry/telemetry_test.go | 16 ++ cli/util/server.go | 58 +++++ cli/version/compatibility.go | 5 + console/src/Endpoints.js | 1 + console/src/Globals.js | 3 + console/src/client.js | 88 ++++++- console/src/components/App/App.js | 12 + console/src/components/Main/Main.js | 13 +- console/src/components/Main/Main.scss | 7 + console/src/components/Main/State.js | 1 + console/src/helpers/Html.js | 3 +- console/src/reducer.js | 2 + console/src/telemetry/Actions.js | 122 ++++++++++ console/src/telemetry/Notifications.js | 40 ++++ console/src/telemetry/State.js | 6 + console/src/telemetryFilter.js | 61 +++++ .../graphql-engine-flags/reference.rst | 4 + docs/graphql/manual/guides/index.rst | 5 + docs/graphql/manual/guides/telemetry.rst | 148 ++++++++++++ server/graphql-engine.cabal | 1 + server/src-exec/Main.hs | 55 +++-- server/src-exec/Ops.hs | 38 ++- server/src-lib/Hasura/Server/App.hs | 23 +- server/src-lib/Hasura/Server/Init.hs | 76 ++++-- server/src-lib/Hasura/Server/Telemetry.hs | 223 ++++++++++++++++++ server/src-lib/Hasura/Server/Version.hs | 5 + server/src-rsr/console.html | 3 +- server/src-rsr/hdb_metadata.yaml | 5 + server/src-rsr/initialise.sql | 5 +- server/src-rsr/migrate_from_8_to_9.sql | 5 + .../src-rsr/migrate_metadata_from_8_to_9.yaml | 4 + server/tests-py/graphql_server.py | 2 +- 48 files changed, 1606 insertions(+), 145 deletions(-) create mode 100755 cli/get.sh create mode 100644 cli/telemetry/telemetry.go create mode 100644 cli/telemetry/telemetry_test.go create mode 100644 cli/util/server.go create mode 100644 console/src/telemetry/Actions.js create mode 100644 console/src/telemetry/Notifications.js create mode 100644 console/src/telemetry/State.js create mode 100644 console/src/telemetryFilter.js create mode 100644 docs/graphql/manual/guides/telemetry.rst create mode 100644 server/src-lib/Hasura/Server/Telemetry.hs create mode 100644 server/src-rsr/migrate_from_8_to_9.sql create mode 100644 server/src-rsr/migrate_metadata_from_8_to_9.yaml diff --git a/cli/Gopkg.lock b/cli/Gopkg.lock index 0a7653f3086..1ec041fa1db 100644 --- a/cli/Gopkg.lock +++ b/cli/Gopkg.lock @@ -2,51 +2,66 @@ [[projects]] + digest = "1:55388fd080150b9a072912f97b1f5891eb0b50df43401f8b75fb4273d3fec9fc" name = "github.com/Masterminds/semver" packages = ["."] + pruneopts = "UT" revision = "c7af12943936e8c39859482e61f0574c2fd7fc75" version = "v1.4.2" [[projects]] + digest = "1:bf42be3cb1519bf8018dfd99720b1005ee028d947124cab3ccf965da59381df6" name = "github.com/Microsoft/go-winio" packages = ["."] + pruneopts = "UT" revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f" version = "v0.4.7" [[projects]] + digest = "1:889290ee5c1f1888baa7caa2b4cdfa8a6abcfb86dd772fe6470ad7925cc44bff" name = "github.com/briandowns/spinner" packages = ["."] + pruneopts = "UT" revision = "48dbb65d7bd5c74ab50d53d04c949f20e3d14944" version = "1.0" [[projects]] branch = "master" + digest = "1:454adc7f974228ff789428b6dc098638c57a64aa0718f0bd61e53d3cd39d7a75" name = "github.com/chzyer/readline" packages = ["."] + pruneopts = "UT" revision = "2972be24d48e78746da79ba8e24e8b488c9880de" [[projects]] + digest = "1:7cb4fdca4c251b3ef8027c90ea35f70c7b661a593b9eeae34753c65499098bb1" name = "github.com/cpuguy83/go-md2man" packages = ["md2man"] + pruneopts = "UT" revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1" version = "v1.0.8" [[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "UT" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] + digest = "1:3cabbabc9e0e4aa7e12b882bdc213f41cf8bd2b2ce2a7b5e0aceaf8a6a78049b" name = "github.com/docker/distribution" packages = [ "digest", - "reference" + "reference", ] + pruneopts = "UT" revision = "48294d928ced5dd9b378f7fd7c6f5da3ff3f2c89" version = "v2.6.2" [[projects]] + digest = "1:c4c7064c2c67a0a00815918bae489dd62cd88d859d24c95115d69b00b3d33334" name = "github.com/docker/docker" packages = [ "api/types", @@ -64,92 +79,126 @@ "api/types/versions", "api/types/volume", "client", - "pkg/tlsconfig" + "pkg/tlsconfig", ] + pruneopts = "UT" revision = "092cba3727bb9b4a2f0e922cd6c0f93ea270e363" version = "v1.13.1" [[projects]] + digest = "1:b6b5c3e8da0fb8073cd2886ba249a40f4402b4391ca6eba905a142cceea97a12" name = "github.com/docker/go-connections" packages = [ "nat", "sockets", - "tlsconfig" + "tlsconfig", ] + pruneopts = "UT" revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d" version = "v0.3.0" [[projects]] + digest = "1:6f82cacd0af5921e99bf3f46748705239b36489464f4529a1589bc895764fb18" name = "github.com/docker/go-units" packages = ["."] + pruneopts = "UT" revision = "47565b4f722fb6ceae66b95f853feed578a4a51c" version = "v0.3.3" [[projects]] + digest = "1:f4f6279cb37479954644babd8f8ef00584ff9fa63555d2c6718c1c3517170202" name = "github.com/elazarl/go-bindata-assetfs" packages = ["."] + pruneopts = "UT" revision = "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43" version = "v1.0.0" [[projects]] + digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a" name = "github.com/fatih/color" packages = ["."] + pruneopts = "UT" revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4" version = "v1.7.0" [[projects]] + digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" name = "github.com/fsnotify/fsnotify" packages = ["."] + pruneopts = "UT" revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" [[projects]] + digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda" name = "github.com/ghodss/yaml" packages = ["."] + pruneopts = "UT" revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" [[projects]] + digest = "1:2b59aca2665ff804f6606c8829eaee133ddd3aefbc841014660d961b0034f888" name = "github.com/gin-contrib/cors" packages = ["."] + pruneopts = "UT" revision = "cf4846e6a636a76237a28d9286f163c132e841bc" version = "v1.2" [[projects]] branch = "master" + digest = "1:36fe9527deed01d2a317617e59304eb2c4ce9f8a24115bcc5c2e37b3aee5bae4" name = "github.com/gin-contrib/sse" packages = ["."] + pruneopts = "UT" revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" [[projects]] branch = "master" + digest = "1:d178cf1d1da54559f7f89b0cf5d30763e03750bbf0692df2f45b1815b8b36462" name = "github.com/gin-contrib/static" packages = ["."] + pruneopts = "UT" revision = "73da7037e716e63aa2b0ffceb630dfa7be299086" [[projects]] branch = "master" + digest = "1:1b45decf351430d074b2afaadf7dcb598755d58b185db5a52bbaa93b7cbbb85a" name = "github.com/gin-gonic/contrib" packages = ["renders/multitemplate"] + pruneopts = "UT" revision = "39cfb9727134fef3120d2458fce5fab14265a46c" [[projects]] + digest = "1:489e108f21464371ebf9cb5c30b1eceb07c6dd772dff073919267493dd9d04ea" name = "github.com/gin-gonic/gin" packages = [ ".", "binding", - "render" + "render", ] + pruneopts = "UT" revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" version = "v1.2" [[projects]] + digest = "1:c594a691090b434d55c67f6cc8e326ef5ba49452abc059821bd5d4fd4cdef08c" + name = "github.com/gofrs/uuid" + packages = ["."] + pruneopts = "UT" + revision = "7077aa61129615a0d7f45c49101cd011ab221c27" + version = "v3.1.2" + +[[projects]] + digest = "1:6ba96a683441984156b05568b9d31dbc846d3336d21ac220fcc819a367dc1f65" name = "github.com/golang/protobuf" packages = ["proto"] + pruneopts = "UT" revision = "5a0f697c9ed9d68fef0116532c6e05cfeae00e55" [[projects]] branch = "master" + digest = "1:a361611b8c8c75a1091f00027767f7779b29cb37c456a71b8f2604c88057ab40" name = "github.com/hashicorp/hcl" packages = [ ".", @@ -161,84 +210,107 @@ "hcl/token", "json/parser", "json/scanner", - "json/token" + "json/token", ] + pruneopts = "UT" revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" [[projects]] + digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" name = "github.com/inconshreveable/mousetrap" packages = ["."] + pruneopts = "UT" revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" version = "v1.0" [[projects]] branch = "master" + digest = "1:e51f40f0c19b39c1825eadd07d5c0a98a2ad5942b166d9fc4f54750ce9a04810" name = "github.com/juju/ansiterm" packages = [ ".", - "tabwriter" + "tabwriter", ] + pruneopts = "UT" revision = "720a0952cc2ac777afc295d9861263e2a4cf96a1" [[projects]] branch = "master" + digest = "1:37ce7d7d80531b227023331002c0d42b4b4b291a96798c82a049d03a54ba79e4" name = "github.com/lib/pq" packages = [ ".", - "oid" + "oid", ] + pruneopts = "UT" revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8" [[projects]] branch = "master" + digest = "1:bb08c7bb1c7224636b1a00639f079ed4391eb822945f26db74b8d8ee3f14d991" name = "github.com/lunixbochs/vtclean" packages = ["."] + pruneopts = "UT" revision = "2d01aacdc34a083dca635ba869909f5fc0cd4f41" [[projects]] + digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7" name = "github.com/magiconair/properties" packages = ["."] + pruneopts = "UT" revision = "c2353362d570a7bfa228149c62842019201cfb71" version = "v1.8.0" [[projects]] + digest = "1:e3294b38b5590a93f57da4a340158802222f3bb41377ebc796dbc6e2e586ee3a" name = "github.com/manifoldco/promptui" packages = [ ".", "list", - "screenbuf" + "screenbuf", ] + pruneopts = "UT" revision = "3dd80c00b7cb0bc779d1c204da6f3ae0fa6a4eee" version = "v0.3.0" [[projects]] + digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67" name = "github.com/mattn/go-colorable" packages = ["."] + pruneopts = "UT" revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" version = "v0.0.9" [[projects]] + digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb" name = "github.com/mattn/go-isatty" packages = ["."] + pruneopts = "UT" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" [[projects]] branch = "master" + digest = "1:8eb17c2ec4df79193ae65b621cd1c0c4697db3bc317fe6afdc76d7f2746abd05" name = "github.com/mitchellh/go-homedir" packages = ["."] + pruneopts = "UT" revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66" [[projects]] branch = "master" + digest = "1:e730597b38a4d56e2361e0b6236cb800e52c73cace2ff91396f4ff35792ddfa7" name = "github.com/mitchellh/mapstructure" packages = ["."] + pruneopts = "UT" revision = "bb74f1db0675b241733089d5a1faa5dd8b0ef57b" [[projects]] branch = "master" + digest = "1:7aefb397a53fc437c90f0fdb3e1419c751c5a3a165ced52325d5d797edf1aca6" name = "github.com/moul/http2curl" packages = ["."] + pruneopts = "UT" revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d" [[projects]] @@ -250,109 +322,141 @@ [[projects]] name = "github.com/parnurzeal/gorequest" packages = ["."] + pruneopts = "UT" revision = "a578a48e8d6ca8b01a3b18314c43c6716bb5f5a3" version = "v0.2.15" [[projects]] + digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" name = "github.com/pelletier/go-toml" packages = ["."] + pruneopts = "UT" revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" version = "v1.2.0" [[projects]] + digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" name = "github.com/pkg/errors" packages = ["."] + pruneopts = "UT" revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" [[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "UT" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] + digest = "1:8bc629776d035c003c7814d4369521afe67fdb8efc4b5f66540d29343b98cf23" name = "github.com/russross/blackfriday" packages = ["."] + pruneopts = "UT" revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5" version = "v1.5.1" [[projects]] + digest = "1:2cf43d017348f06dc503ed7e639717262208f79ddca52ce2f01776e71543b567" name = "github.com/sirupsen/logrus" packages = [ ".", - "hooks/test" + "hooks/test", ] + pruneopts = "UT" revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" version = "v1.0.5" [[projects]] branch = "master" + digest = "1:39853e1ae46a02816e2419e1f590e00682b1a6b60bb988597cf2efb84314da45" name = "github.com/skratchdot/open-golang" packages = ["open"] + pruneopts = "UT" revision = "75fb7ed4208cf72d323d7d02fd1a5964a7a9073c" [[projects]] + digest = "1:bd1ae00087d17c5a748660b8e89e1043e1e5479d0fea743352cda2f8dd8c4f84" name = "github.com/spf13/afero" packages = [ ".", - "mem" + "mem", ] + pruneopts = "UT" revision = "787d034dfe70e44075ccc060d346146ef53270ad" version = "v1.1.1" [[projects]] + digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f" name = "github.com/spf13/cast" packages = ["."] + pruneopts = "UT" revision = "8965335b8c7107321228e3e3702cab9832751bac" version = "v1.2.0" [[projects]] + digest = "1:e01b05ba901239c783dfe56450bcde607fc858908529868259c9a8765dc176d0" name = "github.com/spf13/cobra" packages = [ ".", - "doc" + "doc", ] + pruneopts = "UT" revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" version = "v0.0.3" [[projects]] branch = "master" + digest = "1:080e5f630945ad754f4b920e60b4d3095ba0237ebf88dc462eb28002932e3805" name = "github.com/spf13/jwalterweatherman" packages = ["."] + pruneopts = "UT" revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" [[projects]] + digest = "1:9424f440bba8f7508b69414634aef3b2b3a877e522d8a4624692412805407bb7" name = "github.com/spf13/pflag" packages = ["."] + pruneopts = "UT" revision = "583c0c0531f06d5278b7d917446061adc344b5cd" version = "v1.0.1" [[projects]] + digest = "1:59e7dceb53b4a1e57eb1eb0bf9951ff0c25912df7660004a789b62b4e8cdca3b" name = "github.com/spf13/viper" packages = ["."] + pruneopts = "UT" revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" version = "v1.0.2" [[projects]] + digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" name = "github.com/stretchr/testify" packages = ["assert"] + pruneopts = "UT" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" [[projects]] + digest = "1:c268acaa4a4d94a467980e5e91452eb61c460145765293dc0aed48e5e9919cc6" name = "github.com/ugorji/go" packages = ["codec"] + pruneopts = "UT" revision = "c88ee250d0221a57af388746f5cf03768c21d6e2" [[projects]] branch = "master" + digest = "1:3f3a05ae0b95893d90b9b3b5afdb79a9b3d96e4e36e099d841ae602e4aca0da8" name = "golang.org/x/crypto" packages = ["ssh/terminal"] + pruneopts = "UT" revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" [[projects]] branch = "master" + digest = "1:aa91545b200c0c8e6cf39bb8eedc86d85d3ed0a208662ed964bef0433839fd3c" name = "golang.org/x/net" packages = [ "context", @@ -360,20 +464,24 @@ "idna", "internal/socks", "proxy", - "publicsuffix" + "publicsuffix", ] + pruneopts = "UT" revision = "afe8f62b1d6bbd81f31868121a50b06d8188e1f9" [[projects]] branch = "master" + digest = "1:c1fee1a2c739c7b220dc3de939f659dce271fa8d9de155cb8cfcfb99c941cbc3" name = "golang.org/x/sys" packages = [ "unix", - "windows" + "windows", ] + pruneopts = "UT" revision = "63fc586f45fe72d95d5240a5d5eb95e6503907d3" [[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" name = "golang.org/x/text" packages = [ "collate", @@ -389,20 +497,25 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "UT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] + digest = "1:1b4724d3c8125f6044925f02b485b74bfec9905cbf579d95aafd1a6c8f8447d3" name = "gopkg.in/go-playground/validator.v8" packages = ["."] + pruneopts = "UT" revision = "5f57d2222ad794d0dffb07e664ea05e2ee07d60c" version = "v8.18.1" [[projects]] + digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "UT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" version = "v2.2.1" diff --git a/cli/assets/assets.go b/cli/assets/assets.go index eb2844c0b92..84e7fe09583 100644 --- a/cli/assets/assets.go +++ b/cli/assets/assets.go @@ -70,7 +70,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _assetsUnversionedConsoleHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x55\x5f\x6b\xfb\x36\x14\x7d\xff\x7d\x8a\x8b\xc6\xe8\xcb\x2c\xa5\x5d\x61\xc5\xb1\x03\x63\x30\x06\xdb\xa0\x0f\xeb\x5e\x8b\x2a\x5f\xdb\x37\x95\x25\x4f\x57\x49\xea\x95\x7c\xf7\xe1\x7f\x89\x9b\x0c\xc6\xca\x46\x5e\x74\xff\x48\xe7\x9e\x23\xe5\x38\xab\x63\x63\xc1\x6a\x57\xe5\x02\x5d\xb2\x63\xb1\xf9\x02\x90\xd5\xa8\x8b\x7e\x01\x90\x59\x72\xaf\x10\xd0\xe6\x82\x8c\x77\x02\x62\xd7\x62\x2e\xa8\xd1\x15\xaa\xd6\x55\x02\xea\x80\x65\x2e\xea\x18\x5b\x4e\x95\xe2\xe8\x83\xae\x50\x56\xde\x57\x16\x75\x4b\x2c\x8d\x6f\x54\xad\x79\x17\x74\x52\x05\xdd\xd6\x7f\xd8\x04\x5d\x45\x0e\x95\xf1\x8e\xbd\x45\xa5\x99\x31\xb2\x2a\xf5\xbe\xc7\x90\xc3\xb1\x6a\xc2\x67\x13\xa8\x8d\x63\x00\x07\x72\x85\x3f\xc8\xe7\x67\x74\x7b\xc8\xe1\x7d\xcc\x02\xe8\x96\x7e\xf2\x1c\x53\x78\x7f\x97\xd3\xfa\x78\xfc\x66\x51\x7d\xf4\x21\xa6\x20\xc6\x72\x1f\x1c\x8f\xe2\x54\x37\x96\x7e\xc7\xc0\xe4\xdd\x70\xc0\x39\x5c\x9c\x51\xe8\xa8\xbf\x6f\xe9\x29\xd8\xa1\xe7\x1c\x5e\xf7\x2c\xcf\xfa\x98\x5a\xce\x64\x0c\x32\xff\x8c\xdd\x38\xf3\x1c\x2d\x3a\x76\xc1\x3e\x06\x2c\xe9\x2d\x05\xa1\x16\xc3\x8e\x9a\xfd\xea\x0b\x4c\x41\x18\x4b\x62\xac\x1c\xd7\xa3\x5e\xea\x2c\x58\xa6\xe6\x7b\xcc\x5e\x7c\xd1\xcd\x82\xc6\xce\xe2\xb8\x96\x8d\x26\xf7\x83\x77\x11\x5d\x3c\x89\x59\x10\xb7\x56\x77\x29\xdc\x38\xef\xf0\x66\x3d\xa5\x7d\xab\x0d\xc5\x2e\x85\xd5\x9c\x89\x41\x3b\xa6\x38\x50\x9d\xaa\x20\xef\x56\x0c\x96\x1c\xea\x30\xb6\x1d\xaf\x80\x24\xd7\xfe\xf0\x37\x68\x2f\xd6\x9b\xd7\x6b\xb8\xdb\x4f\xc0\x65\x6a\x22\x39\x46\x05\xed\x81\x8a\x5c\x58\xaf\x0b\x72\x95\x98\x1e\xd3\x58\x30\x56\x33\xe7\xa2\xd5\x15\x26\x73\x03\x0c\xdb\xf3\x49\x59\x68\xc8\x25\x35\x52\x55\xc7\x14\x6e\x57\xab\x7d\x3d\x8f\x74\xa0\x22\xd6\x43\xee\xeb\xf5\x25\x9f\xd2\xe2\xdb\x9c\xd4\x96\x2a\x97\x50\xc4\x86\x53\x30\xe8\x22\x86\xb9\x54\x7a\x17\x93\x52\x37\x64\xbb\x14\x58\x3b\x4e\x18\x03\x95\x73\x79\xbb\xe3\x48\x65\x97\x98\x51\xbb\xcb\xdd\x27\x2a\xfd\xbd\xb6\xda\xcd\x6c\x2e\x19\x4c\x38\x4c\x7f\x62\x0a\x77\xd8\xac\x4f\xf9\x46\x87\x8a\x5c\x12\x7d\x9b\x42\xf2\xed\xb2\x62\xbc\xf5\x21\x85\xaf\x1e\xee\xfb\xdf\x39\xbf\xc0\xfc\x65\xd4\x4b\x4a\x39\x2b\xaa\xfa\x29\x4e\xfa\xaa\x82\xf6\xd3\xab\x5b\x2c\xe7\xeb\x98\x38\x89\x79\xe8\xc5\x1b\x11\x9b\xe5\x86\xb3\x05\x0d\xac\xb8\x46\x8c\x97\xbe\x63\x0a\xb7\x65\x69\xac\xdf\x15\xa5\xd5\x01\x07\xd7\xd1\x5b\xfd\xa6\x2c\xbd\xb0\x1a\xe8\xeb\x03\xb2\x6f\x50\xdd\xcb\xef\xe4\x4a\x19\xfe\x98\x96\x0d\x39\x69\x98\x85\xfa\x17\xb0\x9f\xb2\xbb\xfe\x0f\x3f\x38\xde\xc9\x16\x54\xcf\x7d\x00\x07\x53\xeb\xc0\x18\x73\xf1\xf4\xdb\x8f\xc9\x83\xf8\x68\x83\xc0\xc1\xfc\xf7\xe0\x7b\x74\x85\x0f\x72\x7b\x8d\xbe\x59\xda\xc9\xff\x3c\xc5\x20\xc1\x3f\xcd\x90\xa9\xd1\xc9\x32\xd5\x7f\xb8\x36\x5f\xfe\x0a\x00\x00\xff\xff\xdf\x70\xc8\xd7\xc0\x06\x00\x00") +var _assetsUnversionedConsoleHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x55\x5d\xab\xe3\x36\x10\x7d\xdf\x5f\x21\x54\xca\xbe\xd4\x52\x76\xbb\xd0\xc5\xd7\xbe\x50\x5a\x4a\x4b\x5b\xd8\x87\x4d\x5f\x97\x89\x3c\xb6\x27\x2b\x4b\xae\x46\x49\xae\x7b\xf1\x7f\x2f\xfe\x4a\x9c\xa4\x50\x7a\xe9\x92\x17\xcd\x99\xf1\x9c\x39\x23\x71\x92\xd5\xb1\xb1\xc2\x82\xab\x72\x89\x2e\x39\xb0\x7c\x7c\x25\x44\x56\x23\x14\xc3\x41\x88\xcc\x92\xfb\x2c\x02\xda\x5c\x92\xf1\x4e\x8a\xd8\xb5\x98\x4b\x6a\xa0\x42\xdd\xba\x4a\x8a\x3a\x60\x99\xcb\x3a\xc6\x96\x53\xad\x39\xfa\x00\x15\xaa\xca\xfb\xca\x22\xb4\xc4\xca\xf8\x46\xd7\xc0\x87\x00\x49\x15\xa0\xad\xff\xb4\x09\xba\x8a\x1c\x6a\xe3\x1d\x7b\x8b\x1a\x98\x31\xb2\x2e\xe1\x38\x70\xa8\xb1\xad\x9e\xf9\xd9\x04\x6a\xe3\x14\x88\x13\xb9\xc2\x9f\xd4\xa7\x4f\xe8\x8e\x22\x17\xcf\x13\x2a\x04\xb4\xf4\xb3\xe7\x98\x8a\xe7\x67\x35\x9f\xfb\xfe\x9b\x55\xf6\x83\x0f\x31\x15\x72\x4a\x0f\x41\xdf\xcb\x73\xde\x58\xfa\x03\x03\x93\x77\x63\x83\x4b\xb8\xea\x51\x40\x84\xef\x5b\xda\x06\x3b\xd6\x5c\xc2\xfb\x9a\x75\xaf\x6b\x68\x3d\x93\x31\xc8\xfc\x2b\x76\xd3\xcc\x4b\xb4\xaa\x38\x04\xfb\x21\x60\x49\x4f\xa9\x90\x7a\x35\xec\xb4\xb3\xdf\x7d\x81\xa9\x90\xc6\xd2\x95\x8e\xed\xf6\x97\x1f\x17\x11\xc3\x79\xd5\x0f\x1d\xec\x2c\x7e\x44\x8b\x0d\xc6\x30\xf1\xde\x60\x7d\x3f\x15\xf7\x0f\xd3\xee\xf5\x65\xf9\x99\x5e\xde\x44\xb6\xf3\x45\xb7\x5c\x4e\xec\x2c\x4e\x67\xd5\x00\xb9\x1f\xbc\x8b\xe8\xe2\xf9\x62\x0a\xe2\xd6\x42\x97\x8a\xd7\xce\x3b\x7c\xfd\x30\xc3\xbe\x05\x43\xb1\x4b\xc5\x66\x41\x62\x00\xc7\x14\xc7\xb5\xcd\x59\xa1\xde\x6e\x58\x58\x72\x08\x61\x2a\xeb\xef\x88\x14\xd7\xfe\xf4\x0f\x6c\x3b\xeb\xcd\xe7\x7b\xba\x37\x2f\xa0\xcb\xf4\x2c\x72\x8a\x0a\x3a\x0a\x2a\x72\x69\x3d\x14\xe4\x2a\x39\x3f\xcc\x29\x61\x2c\x30\xe7\xb2\x85\x0a\x93\xa5\x40\x8c\x9f\xe7\x72\xae\x6b\xc8\x25\x35\x52\x55\xc7\x54\xbc\xd9\x6c\x8e\xf5\x32\xd2\x89\x8a\x58\x8f\xd8\xd7\x0f\xb7\x7a\x4a\x8b\x4f\x0b\x08\x96\x2a\x97\x50\xc4\x86\x53\x61\xd0\x45\x0c\x4b\xaa\xf4\x2e\x26\x25\x34\x64\xbb\x54\x30\x38\x4e\x18\x03\x95\x4b\x7a\x7f\xe0\x48\x65\x97\x98\x69\x77\xb7\x5f\x9f\xa5\x0c\xf7\xda\x82\x5b\xd4\xdc\x2a\x98\x79\x98\xfe\xc2\x54\xbc\xc5\xe6\xe1\x8c\x37\x10\x2a\x72\x49\xf4\x6d\x2a\x92\x6f\xd7\x19\xe3\xad\x0f\xa9\xf8\xea\xfd\xbb\xe1\x77\xc1\x57\x9c\xbf\x4d\xfb\x52\x4a\x2d\x1b\xd5\xc3\x14\xe7\xfd\xea\x82\x8e\xf3\xab\x5b\x1d\x97\xeb\x98\x35\xc9\x65\xe8\xd5\x1b\x91\x8f\xeb\x0f\x2e\x76\x36\xaa\xe2\x1a\x31\xde\x7a\x98\x29\xdc\x9e\x95\xb1\xfe\x50\x94\x16\x02\x8e\x0e\x06\x7b\x78\xd2\x96\x76\xac\x47\xf9\x70\x42\xf6\x0d\xea\x77\xea\x3b\xb5\xd1\x86\xaf\x61\xd5\x90\x53\x86\x59\xea\xff\x40\xfb\x22\xeb\x1c\xcc\x63\x74\xcf\xb3\xc5\xe8\x41\xfb\x48\x2e\x4c\x0d\x81\x31\xe6\x72\xfb\xf1\xa7\xe4\xbd\xbc\xb6\x54\xc1\xc1\xfc\xff\xe4\x47\x74\x85\x0f\x6a\x7f\xcf\xfe\xb8\xb6\x93\x2f\x3c\xc5\xb8\x82\x7f\x9b\x21\xd3\x93\x93\x65\x7a\xf8\x13\x7c\x7c\xf5\x77\x00\x00\x00\xff\xff\xe7\x5e\x19\x48\x0c\x07\x00\x00") func assetsUnversionedConsoleHtmlBytes() ([]byte, error) { return bindataRead( @@ -85,12 +85,12 @@ func assetsUnversionedConsoleHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "assets/unversioned/console.html", size: 1728, mode: os.FileMode(436), modTime: time.Unix(1531822928, 0)} + info := bindataFileInfo{name: "assets/unversioned/console.html", size: 1804, mode: os.FileMode(436), modTime: time.Unix(1547699579, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _assetsV10AlphaConsoleHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x55\x5f\x6b\xfb\x36\x14\x7d\xff\x7d\x8a\x8b\xc6\xe8\xcb\x2c\xa5\x5d\x61\xc5\xb1\x03\x63\x30\x06\xdb\xa0\x0f\xeb\x5e\x8b\x2a\x5f\xdb\x37\x95\x25\x4f\x57\x49\xea\x95\x7c\xf7\xe1\x7f\x89\x9b\x0c\xc6\xca\x46\x5e\x74\xff\x48\xe7\x9e\x23\xe5\x38\xab\x63\x63\xc1\x6a\x57\xe5\x02\x5d\xb2\x63\xb1\xf9\x02\x90\xd5\xa8\x8b\x7e\x01\x90\x59\x72\xaf\x10\xd0\xe6\x82\x8c\x77\x02\x62\xd7\x62\x2e\xa8\xd1\x15\xaa\xd6\x55\x02\xea\x80\x65\x2e\xea\x18\x5b\x4e\x95\xe2\xe8\x83\xae\x50\x56\xde\x57\x16\x75\x4b\x2c\x8d\x6f\x54\xad\x79\x17\x74\x52\x05\xdd\xd6\x7f\xd8\x04\x5d\x45\x0e\x95\xf1\x8e\xbd\x45\xa5\x99\x31\xb2\x2a\xf5\xbe\xc7\x90\xc3\xb1\x6a\xc2\x67\x13\xa8\x8d\x63\x00\x07\x72\x85\x3f\xc8\xe7\x67\x74\x7b\xc8\xe1\x7d\xcc\x02\xe8\x96\x7e\xf2\x1c\x53\x78\x7f\x97\xd3\xfa\x78\xfc\x66\x51\x7d\xf4\x21\xa6\x20\xc6\x72\x1f\x1c\x8f\xe2\x54\x37\x96\x7e\xc7\xc0\xe4\xdd\x70\xc0\x39\x5c\x9c\x51\xe8\xa8\xbf\x6f\xe9\x29\xd8\xa1\xe7\x1c\x5e\xf7\x2c\xcf\xfa\x98\x5a\xce\x64\x0c\x32\xff\x8c\xdd\x38\xf3\x1c\x2d\x3a\x76\xc1\x3e\x06\x2c\xe9\x2d\x05\xa1\x16\xc3\x8e\x9a\xfd\xea\x0b\x4c\x41\x18\x4b\x62\xac\x1c\xd7\xa3\x5e\xea\x2c\x58\xa6\xe6\x7b\xcc\x5e\x7c\xd1\xcd\x82\xc6\xce\xe2\xb8\x96\x8d\x26\xf7\x83\x77\x11\x5d\x3c\x89\x59\x10\xb7\x56\x77\x29\xdc\x38\xef\xf0\x66\x3d\xa5\x7d\xab\x0d\xc5\x2e\x85\xd5\x9c\x89\x41\x3b\xa6\x38\x50\x9d\xaa\x20\xef\x56\x0c\x96\x1c\xea\x30\xb6\x1d\xaf\x80\x24\xd7\xfe\xf0\x37\x68\x2f\xd6\x9b\xd7\x6b\xb8\xdb\x4f\xc0\x65\x6a\x22\x39\x46\x05\xed\x81\x8a\x5c\x58\xaf\x0b\x72\x95\x98\x1e\xd3\x58\x30\x56\x33\xe7\xa2\xd5\x15\x26\x73\x03\x0c\xdb\xf3\x49\x59\x68\xc8\x25\x35\x52\x55\xc7\x14\x6e\x57\xab\x7d\x3d\x8f\x74\xa0\x22\xd6\x43\xee\xeb\xf5\x25\x9f\xd2\xe2\xdb\x9c\xd4\x96\x2a\x97\x50\xc4\x86\x53\x30\xe8\x22\x86\xb9\x54\x7a\x17\x93\x52\x37\x64\xbb\x14\x58\x3b\x4e\x18\x03\x95\x73\x79\xbb\xe3\x48\x65\x97\x98\x51\xbb\xcb\xdd\x27\x2a\xfd\xbd\xb6\xda\xcd\x6c\x2e\x19\x4c\x38\x4c\x7f\x62\x0a\x77\xd8\xac\x4f\xf9\x46\x87\x8a\x5c\x12\x7d\x9b\x42\xf2\xed\xb2\x62\xbc\xf5\x21\x85\xaf\x1e\xee\xfb\xdf\x39\xbf\xc0\xfc\x65\xd4\x4b\x4a\x39\x2b\xaa\xfa\x29\x4e\xfa\xaa\x82\xf6\xd3\xab\x5b\x2c\xe7\xeb\x98\x38\x89\x79\xe8\xc5\x1b\x11\x9b\xe5\x86\xb3\x05\x0d\xac\xb8\x46\x8c\x97\xbe\x63\x0a\xb7\x65\x69\xac\xdf\x15\xa5\xd5\x01\x07\xd7\xd1\x5b\xfd\xa6\x2c\xbd\xb0\x1a\xe8\xeb\x03\xb2\x6f\x50\xdd\xcb\xef\xe4\x4a\x19\xfe\x98\x96\x0d\x39\x69\x98\x85\xfa\x17\xb0\x9f\xb2\xbb\xfe\x0f\x3f\x38\xde\xc9\x16\x54\xcf\x7d\x00\x07\x53\xeb\xc0\x18\x73\xf1\xf4\xdb\x8f\xc9\x83\xf8\x68\x83\xc0\xc1\xfc\xf7\xe0\x7b\x74\x85\x0f\x72\x7b\x8d\xbe\x59\xda\xc9\xff\x3c\xc5\x20\xc1\x3f\xcd\x90\xa9\xd1\xc9\x32\xd5\x7f\xb8\x36\x5f\xfe\x0a\x00\x00\xff\xff\xdf\x70\xc8\xd7\xc0\x06\x00\x00") +var _assetsV10AlphaConsoleHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x55\x5d\xab\xe3\x36\x10\x7d\xdf\x5f\x21\x54\xca\xbe\xd4\x52\x76\xbb\xd0\xc5\xd7\xbe\x50\x5a\x4a\x4b\x5b\xd8\x87\x4d\x5f\x97\x89\x3c\xb6\x27\x2b\x4b\xae\x46\x49\xae\x7b\xf1\x7f\x2f\xfe\x4a\x9c\xa4\x50\x7a\xe9\x92\x17\xcd\x99\xf1\x9c\x39\x23\x71\x92\xd5\xb1\xb1\xc2\x82\xab\x72\x89\x2e\x39\xb0\x7c\x7c\x25\x44\x56\x23\x14\xc3\x41\x88\xcc\x92\xfb\x2c\x02\xda\x5c\x92\xf1\x4e\x8a\xd8\xb5\x98\x4b\x6a\xa0\x42\xdd\xba\x4a\x8a\x3a\x60\x99\xcb\x3a\xc6\x96\x53\xad\x39\xfa\x00\x15\xaa\xca\xfb\xca\x22\xb4\xc4\xca\xf8\x46\xd7\xc0\x87\x00\x49\x15\xa0\xad\xff\xb4\x09\xba\x8a\x1c\x6a\xe3\x1d\x7b\x8b\x1a\x98\x31\xb2\x2e\xe1\x38\x70\xa8\xb1\xad\x9e\xf9\xd9\x04\x6a\xe3\x14\x88\x13\xb9\xc2\x9f\xd4\xa7\x4f\xe8\x8e\x22\x17\xcf\x13\x2a\x04\xb4\xf4\xb3\xe7\x98\x8a\xe7\x67\x35\x9f\xfb\xfe\x9b\x55\xf6\x83\x0f\x31\x15\x72\x4a\x0f\x41\xdf\xcb\x73\xde\x58\xfa\x03\x03\x93\x77\x63\x83\x4b\xb8\xea\x51\x40\x84\xef\x5b\xda\x06\x3b\xd6\x5c\xc2\xfb\x9a\x75\xaf\x6b\x68\x3d\x93\x31\xc8\xfc\x2b\x76\xd3\xcc\x4b\xb4\xaa\x38\x04\xfb\x21\x60\x49\x4f\xa9\x90\x7a\x35\xec\xb4\xb3\xdf\x7d\x81\xa9\x90\xc6\xd2\x95\x8e\xed\xf6\x97\x1f\x17\x11\xc3\x79\xd5\x0f\x1d\xec\x2c\x7e\x44\x8b\x0d\xc6\x30\xf1\xde\x60\x7d\x3f\x15\xf7\x0f\xd3\xee\xf5\x65\xf9\x99\x5e\xde\x44\xb6\xf3\x45\xb7\x5c\x4e\xec\x2c\x4e\x67\xd5\x00\xb9\x1f\xbc\x8b\xe8\xe2\xf9\x62\x0a\xe2\xd6\x42\x97\x8a\xd7\xce\x3b\x7c\xfd\x30\xc3\xbe\x05\x43\xb1\x4b\xc5\x66\x41\x62\x00\xc7\x14\xc7\xb5\xcd\x59\xa1\xde\x6e\x58\x58\x72\x08\x61\x2a\xeb\xef\x88\x14\xd7\xfe\xf4\x0f\x6c\x3b\xeb\xcd\xe7\x7b\xba\x37\x2f\xa0\xcb\xf4\x2c\x72\x8a\x0a\x3a\x0a\x2a\x72\x69\x3d\x14\xe4\x2a\x39\x3f\xcc\x29\x61\x2c\x30\xe7\xb2\x85\x0a\x93\xa5\x40\x8c\x9f\xe7\x72\xae\x6b\xc8\x25\x35\x52\x55\xc7\x54\xbc\xd9\x6c\x8e\xf5\x32\xd2\x89\x8a\x58\x8f\xd8\xd7\x0f\xb7\x7a\x4a\x8b\x4f\x0b\x08\x96\x2a\x97\x50\xc4\x86\x53\x61\xd0\x45\x0c\x4b\xaa\xf4\x2e\x26\x25\x34\x64\xbb\x54\x30\x38\x4e\x18\x03\x95\x4b\x7a\x7f\xe0\x48\x65\x97\x98\x69\x77\xb7\x5f\x9f\xa5\x0c\xf7\xda\x82\x5b\xd4\xdc\x2a\x98\x79\x98\xfe\xc2\x54\xbc\xc5\xe6\xe1\x8c\x37\x10\x2a\x72\x49\xf4\x6d\x2a\x92\x6f\xd7\x19\xe3\xad\x0f\xa9\xf8\xea\xfd\xbb\xe1\x77\xc1\x57\x9c\xbf\x4d\xfb\x52\x4a\x2d\x1b\xd5\xc3\x14\xe7\xfd\xea\x82\x8e\xf3\xab\x5b\x1d\x97\xeb\x98\x35\xc9\x65\xe8\xd5\x1b\x91\x8f\xeb\x0f\x2e\x76\x36\xaa\xe2\x1a\x31\xde\x7a\x98\x29\xdc\x9e\x95\xb1\xfe\x50\x94\x16\x02\x8e\x0e\x06\x7b\x78\xd2\x96\x76\xac\x47\xf9\x70\x42\xf6\x0d\xea\x77\xea\x3b\xb5\xd1\x86\xaf\x61\xd5\x90\x53\x86\x59\xea\xff\x40\xfb\x22\xeb\x1c\xcc\x63\x74\xcf\xb3\xc5\xe8\x41\xfb\x48\x2e\x4c\x0d\x81\x31\xe6\x72\xfb\xf1\xa7\xe4\xbd\xbc\xb6\x54\xc1\xc1\xfc\xff\xe4\x47\x74\x85\x0f\x6a\x7f\xcf\xfe\xb8\xb6\x93\x2f\x3c\xc5\xb8\x82\x7f\x9b\x21\xd3\x93\x93\x65\x7a\xf8\x13\x7c\x7c\xf5\x77\x00\x00\x00\xff\xff\xe7\x5e\x19\x48\x0c\x07\x00\x00") func assetsV10AlphaConsoleHtmlBytes() ([]byte, error) { return bindataRead( @@ -105,12 +105,12 @@ func assetsV10AlphaConsoleHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "assets/v1.0-alpha/console.html", size: 1728, mode: os.FileMode(436), modTime: time.Unix(1531338351, 0)} + info := bindataFileInfo{name: "assets/v1.0-alpha/console.html", size: 1804, mode: os.FileMode(436), modTime: time.Unix(1547699579, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _assetsV10ConsoleHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x55\xdf\x8f\xdc\x34\x10\x7e\xbf\xbf\x62\x64\x84\xfa\x42\xec\x6d\xa9\x44\x95\xdb\x9c\x84\x40\x08\x09\x90\x2a\x41\x79\xad\xe6\x9c\x49\x32\x57\xc7\x0e\x1e\xef\xee\x2d\xab\xfc\xef\x28\xbf\x76\x73\x77\x40\xe1\xa0\xda\x17\xcf\x2f\xcf\x7c\xdf\x78\xbf\x6c\x9b\xd4\x3a\x70\xe8\xeb\x42\x91\xcf\x76\xa2\x6e\xae\x00\xb6\x0d\x61\x39\x1c\x00\xb6\x8e\xfd\x07\x88\xe4\x0a\xc5\x36\x78\x05\xe9\xd8\x51\xa1\xb8\xc5\x9a\x4c\xe7\x6b\x05\x4d\xa4\xaa\x50\x4d\x4a\x9d\xe4\xc6\x48\x0a\x11\x6b\xd2\x75\x08\xb5\x23\xec\x58\xb4\x0d\xad\x69\x50\x76\x11\xb3\x3a\x62\xd7\xfc\xe6\x32\xf2\x35\x7b\x32\x36\x78\x09\x8e\x0c\x8a\x50\x12\x53\xe1\x7e\xe8\xa1\xc7\x6b\xcd\xdc\x5f\x6c\xe4\x2e\x4d\x06\x1c\xd8\x97\xe1\xa0\xdf\xbf\x27\xbf\x87\x02\x4e\x93\x17\x00\x3b\xfe\x3e\x48\xca\xe1\x74\xd2\xf3\xb9\xef\xbf\x58\x45\xdf\x86\x98\x72\x50\x53\x78\x30\xfa\x5e\x9d\xe3\xd6\xf1\xaf\x14\x85\x83\x1f\x2f\xb8\x98\xab\x3b\x4a\x4c\xf8\x75\xc7\xef\xa2\x1b\x73\x2e\xe6\xd3\x9c\xf5\x5d\x0f\x5d\xeb\x99\xac\x25\x91\x1f\xe8\x38\xcd\xbc\x58\xab\x8c\x5d\x74\x6f\x23\x55\x7c\x9f\x83\x32\xab\x61\x27\xce\x7e\x0a\x25\xe5\xa0\xac\x63\x35\x45\xfa\xeb\x89\x2f\x73\x21\x6c\x6b\x96\x3d\x6e\x6f\x43\x79\x5c\x08\x4d\x47\x47\xd3\x59\xb7\xc8\xfe\x9b\xe0\x13\xf9\x74\x26\xb3\x64\xe9\x1c\x1e\x73\x78\xe1\x83\xa7\x17\xd7\xb3\x3b\x74\x68\x39\x1d\x73\xd8\x2c\x9e\x14\xd1\x0b\xa7\x11\xea\x1c\x05\xfd\x6a\x23\xe0\xd8\x13\xc6\x29\xad\x7f\xd2\x48\x4b\x13\x0e\x7f\xd2\xed\xd6\x05\xfb\xe1\x69\xbb\x97\xcf\x68\xb7\x35\x33\xc8\xc9\x2a\x79\x0f\x5c\x16\xca\x05\x2c\xd9\xd7\x6a\x7e\x4c\x53\xc0\x3a\x14\x29\x54\x87\x35\x65\x4b\x02\x8c\xe5\xc5\xcc\x2c\xb4\xec\xb3\x86\xb8\x6e\x52\x0e\x2f\x37\x9b\x7d\xb3\x8c\x74\xe0\x32\x35\xa3\xef\xf3\xeb\xc7\x78\x2a\x47\xf7\x8b\x13\x1d\xd7\x3e\xe3\x44\xad\xe4\x60\xc9\x27\x8a\x4b\xa8\x0a\x3e\x65\x15\xb6\xec\x8e\x39\x08\x7a\xc9\x84\x22\x57\x4b\xf8\x6e\x27\x89\xab\x63\x66\x27\xee\x1e\x57\x9f\xa1\x0c\x7b\xed\xd0\x2f\x68\x1e\x23\x98\xfb\x08\xff\x4e\x39\xbc\xa2\xf6\xfa\xec\x6f\x31\xd6\xec\xb3\x14\xba\x1c\xb2\x2f\xd7\x11\x1b\x5c\x88\x39\x7c\xf6\xe6\xf5\xf0\xbb\xf8\x57\x3d\x7f\x9c\xf8\xd2\x5a\x2f\x8c\x9a\x61\x8a\x33\xbf\xa6\xe4\xfd\xfc\xea\x56\xc7\x65\x1d\x33\x26\xb5\x0c\xbd\x7a\x23\xea\x66\x5d\x70\x91\xa0\x11\x95\x34\x44\xe9\xb1\xee\xd8\xd2\xdf\x89\xb6\x2e\xec\xca\xca\x61\xa4\x51\x75\xf0\x0e\xef\x8d\xe3\x5b\x31\x23\x7c\x3c\x90\x84\x96\xcc\x6b\xfd\x95\xde\x18\x2b\x0f\xdd\xba\x65\xaf\xad\x88\x32\xf3\xbb\x39\x9d\x80\x2b\x18\xe4\xe0\xe7\x84\x89\xed\xb7\x1c\xa1\xef\xaf\x3e\x3e\x93\x91\x31\xdf\x0c\x80\xc6\x1b\xc1\x36\x18\x85\x52\xa1\xde\xfd\xf2\x5d\xf6\x46\x3d\xd4\x36\x90\x68\x2f\x45\x7b\xf2\x65\x88\xfa\xee\x69\xd5\xcd\xfa\xbf\xfd\x17\xd5\x63\xcb\xbf\xaf\x5d\xb0\x91\x13\xfa\x67\x78\xfe\x93\xb6\x0f\xea\x36\xca\xfb\x59\x03\xff\x1d\x2f\xff\x77\xf3\xe7\xf1\xfb\x49\x28\xf8\xf8\x9e\x86\x35\xf9\x72\xde\xd2\xd6\x4c\x2a\xbe\x35\xc3\x47\xfb\xe6\xea\x8f\x00\x00\x00\xff\xff\x65\x0e\x41\xb9\xbc\x07\x00\x00") +var _assetsV10ConsoleHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x56\x5f\x8f\xe3\x34\x10\x7f\xdf\x4f\x31\x32\x42\xf7\x42\xec\xde\x71\x12\xa7\x6c\xbb\x12\xe2\x84\x40\x80\x74\x12\xb7\xbc\x9e\x66\x9d\x49\x32\x7b\x8e\x1d\x3c\x6e\xbb\xa5\xca\x77\x47\xf9\xd7\x66\xbb\xc0\xc1\x02\xea\x8b\xe7\xff\xfc\x7e\xe3\x8c\xbb\xae\x53\xe3\xc0\xa1\xaf\x36\x8a\x7c\xb6\x15\x75\x73\x05\xb0\xae\x09\x8b\xfe\x00\xb0\x76\xec\x3f\x42\x24\xb7\x51\x6c\x83\x57\x90\x0e\x2d\x6d\x14\x37\x58\x91\x69\x7d\xa5\xa0\x8e\x54\x6e\x54\x9d\x52\x2b\xb9\x31\x92\x42\xc4\x8a\x74\x15\x42\xe5\x08\x5b\x16\x6d\x43\x63\x6a\x94\x6d\xc4\xac\x8a\xd8\xd6\xbf\xba\x8c\x7c\xc5\x9e\x8c\x0d\x5e\x82\x23\x83\x22\x94\xc4\x94\xb8\xeb\x6b\xe8\x21\xad\x99\xea\x8b\x8d\xdc\xa6\x51\x80\x3d\xfb\x22\xec\xf5\x87\x0f\xe4\x77\xb0\x81\xe3\xa8\x05\xc0\x96\xbf\x0b\x92\x72\x38\x1e\xf5\x74\xee\xba\x2f\x16\xd6\x77\x21\xa6\x1c\xd4\x68\xee\x85\xae\x53\x27\xbb\x75\xfc\x0b\x45\xe1\xe0\x87\x04\x67\x71\x91\xa3\xc0\x84\x5f\xb7\x7c\x1b\xdd\xe0\x73\x16\x9f\xfa\x2c\x73\x3d\x56\x2d\x7b\xb2\x96\x44\x7e\xa0\xc3\xd8\xf3\x2c\x2d\x3c\xb6\xd1\xbd\x8b\x54\xf2\x43\x0e\xca\x2c\x9a\x1d\x39\xfb\x29\x14\x94\x83\xb2\x8e\x1f\xe1\xb8\xbd\xfd\xfe\xed\x0c\xa2\x3f\x2f\xf2\x91\xc7\x3b\x47\xef\xc9\x51\x43\x29\x8e\x75\x2f\x74\x5d\x37\x3a\x77\xd7\x23\xf7\xe6\x4c\xfe\xda\xcc\x77\x62\x7d\x17\x8a\xc3\x3c\x9c\x74\x70\x34\x9e\x75\x83\xec\xbf\x09\x3e\x91\x4f\xa7\xc1\x14\x2c\xad\xc3\x43\x0e\x2f\x7c\xf0\xf4\xe2\x7a\x52\x87\x16\x2d\xa7\x43\x0e\xab\x59\x93\x22\x7a\xe1\x34\xd0\x36\x59\x41\xbf\x5a\x09\x38\xf6\x84\x71\x74\xeb\x9e\x14\xd2\x52\x87\xfd\x1f\x54\xbb\x73\xc1\x7e\x7c\x5a\xee\xe5\x33\xca\xad\xcd\x04\x72\x94\x0a\xde\x01\x17\x1b\xe5\x02\x16\xec\x2b\x35\x5d\xcc\xd1\x60\x1d\x8a\x6c\x54\x8b\x15\x65\xb3\x03\x0c\xe1\x1b\x35\xf9\x35\xec\xb3\x9a\xb8\xaa\x53\x0e\x2f\x57\xab\x5d\x3d\xb7\xb4\xe7\x22\xd5\x83\xee\xf3\xeb\x4b\x3c\xa5\xa3\x87\x59\x89\x8e\x2b\x9f\x71\xa2\x46\x72\xb0\xe4\x13\xc5\xd9\x54\x06\x9f\xb2\x12\x1b\x76\x87\x1c\x04\xbd\x64\x42\x91\xcb\xd9\x7c\xbf\x95\xc4\xe5\x21\xb3\x23\x77\x97\xd1\x27\x28\xfd\x5c\x5b\xf4\x33\x9a\x4b\x04\x53\x1d\xe1\xdf\x28\x87\x57\xd4\x5c\x9f\xf4\x0d\xc6\x8a\x7d\x96\x42\x9b\x43\xf6\xe5\xd2\x62\x83\x0b\x31\x87\xcf\xde\xbc\xee\x7f\x67\xfd\xa2\xe6\x8f\x23\x5f\x5a\xeb\x99\x51\xd3\x77\x71\xe2\xd7\x14\xbc\x9b\x6e\xdd\xe2\x38\x8f\x63\xc2\xa4\xe6\xa6\x17\x77\x44\xdd\x2c\x03\xce\xeb\x6c\x40\x25\x35\x51\xba\xdc\x61\xb6\xf0\xf7\xa2\xad\x0b\xdb\xa2\x74\x18\x69\xd8\x60\x78\x8f\x0f\xc6\xf1\x9d\x98\x01\x3e\xee\x49\x42\x43\xe6\xb5\xfe\x4a\xaf\x8c\x95\xc7\x6a\xdd\xb0\xd7\x56\x44\x99\xe9\xde\x1c\x8f\xc0\x25\xf4\x5f\xe5\xcf\x09\x13\xdb\xb7\x1c\xa1\xeb\xae\x3e\xdd\x93\x91\xc1\xdf\xf4\x80\x86\x8c\x60\x6b\x8c\x42\x69\xa3\x6e\xdf\x7f\x9b\xbd\x51\x8f\xf7\x24\x48\xb4\xe7\xa0\x1d\xf9\x22\x44\x7d\xff\x34\xea\x66\xf9\x6d\xff\x49\xf4\x50\xf2\xaf\x63\x67\x6c\xe4\x84\xfe\x1e\x9e\x7f\xf5\x4e\xf4\x9b\x72\x78\x2a\x4e\xfb\xf4\x9f\xf1\xf2\x5f\x17\x7f\x1e\xbf\xff\x0b\x05\x9f\x9e\x53\x3f\x26\x5f\x4c\x53\x5a\x9b\x71\x8b\xaf\x4d\xff\x07\xe0\xe6\xea\xf7\x00\x00\x00\xff\xff\xe7\xc6\x9b\x76\x08\x08\x00\x00") func assetsV10ConsoleHtmlBytes() ([]byte, error) { return bindataRead( @@ -125,7 +125,7 @@ func assetsV10ConsoleHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "assets/v1.0/console.html", size: 1980, mode: os.FileMode(436), modTime: time.Unix(1531907113, 0)} + info := bindataFileInfo{name: "assets/v1.0/console.html", size: 2056, mode: os.FileMode(436), modTime: time.Unix(1547699579, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/cli/assets/unversioned/console.html b/cli/assets/unversioned/console.html index 473ba25d3ce..264ea553f06 100644 --- a/cli/assets/unversioned/console.html +++ b/cli/assets/unversioned/console.html @@ -10,7 +10,9 @@ dataApiVersion: {{.dataApiVersion}}, accessKey: {{.accessKey}}, urlPrefix: "/", - consoleMode: "cli" + consoleMode: "cli", + cliUUID: {{.cliUUID}}, + enableTelemetry: {{.enableTelemetry}} }; diff --git a/cli/assets/v1.0-alpha/console.html b/cli/assets/v1.0-alpha/console.html index 473ba25d3ce..264ea553f06 100644 --- a/cli/assets/v1.0-alpha/console.html +++ b/cli/assets/v1.0-alpha/console.html @@ -10,7 +10,9 @@ dataApiVersion: {{.dataApiVersion}}, accessKey: {{.accessKey}}, urlPrefix: "/", - consoleMode: "cli" + consoleMode: "cli", + cliUUID: {{.cliUUID}}, + enableTelemetry: {{.enableTelemetry}} }; diff --git a/cli/assets/v1.0/console.html b/cli/assets/v1.0/console.html index f62efd5486d..d792602b54a 100644 --- a/cli/assets/v1.0/console.html +++ b/cli/assets/v1.0/console.html @@ -10,7 +10,9 @@ dataApiVersion: {{.dataApiVersion}}, accessKey: {{.accessKey}}, urlPrefix: "/", - consoleMode: "cli" + consoleMode: "cli", + cliUUID: {{.cliUUID}}, + enableTelemetry: {{.enableTelemetry}} }; diff --git a/cli/cli.go b/cli/cli.go index 2ecf6b91937..a1796850fcc 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -8,6 +8,8 @@ package cli import ( + "encoding/json" + "io/ioutil" "net/url" "os" "path/filepath" @@ -15,7 +17,11 @@ import ( "strings" "time" + "github.com/hasura/graphql-engine/cli/telemetry" + "github.com/hasura/graphql-engine/cli/util" + "github.com/briandowns/spinner" + "github.com/gofrs/uuid" "github.com/hasura/graphql-engine/cli/version" colorable "github.com/mattn/go-colorable" homedir "github.com/mitchellh/go-homedir" @@ -37,11 +43,19 @@ const ( // Other constants used in the package const ( // Name of the global configuration directory - GLOBAL_CONFIG_DIR_NAME = ".hasura-graphql" + GLOBAL_CONFIG_DIR_NAME = ".hasura" // Name of the global configuration file GLOBAL_CONFIG_FILE_NAME = "config.json" ) +// String constants +const ( + StrTelemetryNotice = `Help us improve Hasura! The cli collects anonymized usage stats which +allow us to keep improving Hasura at warp speed. To opt-out or read more, +visit https://docs.hasura.io/1.0/graphql/manual/guides/telemetry.html +` +) + // HasuraGraphQLConfig has the config values required to contact the server. type HasuraGraphQLConfig struct { // Endpoint for the GraphQL Engine @@ -62,6 +76,15 @@ func (hgc *HasuraGraphQLConfig) ParseEndpoint() error { return nil } +// GlobalConfig is the configuration object stored in the GlobalConfigFile. +type GlobalConfig struct { + // UUID used for telemetry, generated on first run. + UUID string `json:"uuid"` + + // Indicate if telemetry is enabled or not + EnableTelemetry bool `json:"enable_telemetry"` +} + // ExecutionContext contains various contextual information required by the cli // at various points of it's execution. Values are filled in by the // initializers and passed on to each command. Commands can also fill in values @@ -71,6 +94,12 @@ type ExecutionContext struct { // correctly render example strings etc. CMDName string + // ID is a unique ID for this Execution + ID string + + // ServerUUID is the unique ID for the server this execution is contacting. + ServerUUID string + // Spinner is the global spinner object used to show progress across the cli. Spinner *spinner.Spinner // Logger is the global logger object to print logs. @@ -96,6 +125,9 @@ type ExecutionContext struct { // stored. GlobalConfigFile string + // GlobalConfig holds all the configuration options. + GlobalConfig *GlobalConfig + // IsStableRelease indicates if the CLI release is stable or not. IsStableRelease bool // Version indicates the version object @@ -106,6 +138,17 @@ type ExecutionContext struct { // LogLevel indicates the logrus default logging level LogLevel string + + // Telemetry collects the telemetry data throughout the execution + Telemetry *telemetry.Data +} + +// NewExecutionContext returns a new instance of execution context +func NewExecutionContext() *ExecutionContext { + ec := &ExecutionContext{} + ec.Telemetry = telemetry.BuildEvent() + ec.Telemetry.Version = version.BuildVersion + return ec } // Prepare as the name suggests, prepares the ExecutionContext ec by @@ -128,14 +171,37 @@ func (ec *ExecutionContext) Prepare() error { // populate version ec.setVersion() - // setup global config directory - err := ec.setupGlobalConfigDir() + // setup global config + err := ec.setupGlobalConfig() if err != nil { - return errors.Wrap(err, "setting up config directory failed") + // TODO(shahidhk): should this be a failure? + return errors.Wrap(err, "setting up global config directory failed") + } + + // read global config + err = ec.readGlobalConfig() + if err != nil { + return errors.Wrap(err, "reading global config failed") } // initialize a blank config - ec.Config = &HasuraGraphQLConfig{} + if ec.Config == nil { + ec.Config = &HasuraGraphQLConfig{} + } + + // generate an execution id + if ec.ID == "" { + id := "00000000-0000-0000-0000-000000000000" + u, err := uuid.NewV4() + if err == nil { + id = u.String() + } else { + ec.Logger.Debugf("generating uuid for execution ID failed, %v", err) + } + ec.ID = id + ec.Logger.Debugf("execution id: %v", ec.ID) + } + ec.Telemetry.ExecutionID = ec.ID return nil } @@ -171,7 +237,17 @@ func (ec *ExecutionContext) Validate() error { ec.Logger.Debug("graphql engine access_key: ", ec.Config.AccessKey) // get version from the server and match with the cli version - return ec.checkServerVersion() + err = ec.checkServerVersion() + if err != nil { + return errors.Wrap(err, "version check") + } + + state := util.GetServerState(ec.Config.Endpoint, ec.Config.AccessKey, ec.Version.ServerSemver, ec.Logger) + ec.ServerUUID = state.UUID + ec.Telemetry.ServerUUID = ec.ServerUUID + ec.Logger.Debugf("server: uuid: %s", ec.ServerUUID) + + return nil } func (ec *ExecutionContext) checkServerVersion() error { @@ -180,6 +256,7 @@ func (ec *ExecutionContext) checkServerVersion() error { return errors.Wrap(err, "failed to get version from server") } ec.Version.SetServerVersion(v) + ec.Telemetry.ServerVersion = ec.Version.GetServerVersion() isCompatible, reason := ec.Version.CheckCLIServerCompatibility() ec.Logger.Debugf("versions: cli: [%s] server: [%s]", ec.Version.GetCLIVersion(), ec.Version.GetServerVersion()) ec.Logger.Debugf("compatibility check: [%v] %v", isCompatible, reason) @@ -189,27 +266,56 @@ func (ec *ExecutionContext) checkServerVersion() error { return nil } +// readGlobalConfig reads the configuration from global config file env vars, +// through viper. +func (ec *ExecutionContext) readGlobalConfig() error { + // need to get existing viper because https://github.com/spf13/viper/issues/233 + v := viper.New() + v.SetEnvPrefix("HASURA_GRAPHQL") + v.AutomaticEnv() + v.SetConfigName("config") + v.AddConfigPath(ec.GlobalConfigDir) + err := v.ReadInConfig() + if err != nil { + return errors.Wrap(err, "cannor read global config from file/env") + } + if ec.GlobalConfig == nil { + ec.Logger.Debugf("global config is not pre-set, reading from current env") + ec.GlobalConfig = &GlobalConfig{ + UUID: v.GetString("uuid"), + EnableTelemetry: v.GetBool("enable_telemetry"), + } + } else { + ec.Logger.Debugf("global config is pre-set to %#v", ec.GlobalConfig) + } + ec.Logger.Debugf("global config: uuid: %v", ec.GlobalConfig.UUID) + ec.Logger.Debugf("global config: enableTelemetry: %v", ec.GlobalConfig.EnableTelemetry) + // set if telemetry can be beamed or not + ec.Telemetry.CanBeam = ec.GlobalConfig.EnableTelemetry + ec.Telemetry.UUID = ec.GlobalConfig.UUID + return nil +} + // readConfig reads the configuration from config file, flags and env vars, // through viper. func (ec *ExecutionContext) readConfig() error { // need to get existing viper because https://github.com/spf13/viper/issues/233 v := ec.Viper - v.SetDefault("endpoint", "http://localhost:8080") - v.SetDefault("access_key", "") v.SetEnvPrefix("HASURA_GRAPHQL") v.AutomaticEnv() v.SetConfigName("config") + v.SetDefault("endpoint", "http://localhost:8080") + v.SetDefault("access_key", "") v.AddConfigPath(ec.ExecutionDirectory) err := v.ReadInConfig() if err != nil { - return errors.Wrap(err, "cannor read config file") + return errors.Wrap(err, "cannor read config from file/env") } ec.Config = &HasuraGraphQLConfig{ Endpoint: v.GetString("endpoint"), AccessKey: v.GetString("access_key"), } - err = ec.Config.ParseEndpoint() - return err + return ec.Config.ParseEndpoint() } // setupSpinner creates a default spinner if the context does not already have @@ -249,24 +355,90 @@ func (ec *ExecutionContext) setupLogger() { } ec.Logger.SetLevel(level) } + + // set the logger for telemetry + if ec.Telemetry.Logger == nil { + ec.Telemetry.Logger = ec.Logger + } } -// setupGlobalConfigDir ensures that global config directory exists and the -// paths are correctly set. -func (ec *ExecutionContext) setupGlobalConfigDir() error { +// setupGlobConfig ensures that global config directory and file exists and +// reads it into the GlobalConfig object. +func (ec *ExecutionContext) setupGlobalConfig() error { if len(ec.GlobalConfigDir) == 0 { + ec.Logger.Debug("global config directory is not pre-set, defaulting") home, err := homedir.Dir() if err != nil { return errors.Wrap(err, "cannot get home directory") } globalConfigDir := filepath.Join(home, GLOBAL_CONFIG_DIR_NAME) ec.GlobalConfigDir = globalConfigDir + ec.Logger.Debugf("global config directory set as '%s'", ec.GlobalConfigDir) } err := os.MkdirAll(ec.GlobalConfigDir, os.ModePerm) if err != nil { - return errors.Wrap(err, "cannot create config directory") + return errors.Wrap(err, "cannot create global config directory") + } + if len(ec.GlobalConfigFile) == 0 { + ec.GlobalConfigFile = filepath.Join(ec.GlobalConfigDir, GLOBAL_CONFIG_FILE_NAME) + ec.Logger.Debugf("global config file set as '%s'", ec.GlobalConfigFile) + } + _, err = os.Stat(ec.GlobalConfigFile) + if os.IsNotExist(err) { + // file does not exist, teat as first run and create it + ec.Logger.Debug("global config file does not exist, this could be the first run, creating it...") + u, err := uuid.NewV4() + if err != nil { + return errors.Wrap(err, "failed to generate uuid") + } + gc := GlobalConfig{ + UUID: u.String(), + EnableTelemetry: true, + } + data, err := json.MarshalIndent(gc, "", " ") + if err != nil { + return errors.Wrap(err, "cannot marshal json for config file") + } + err = ioutil.WriteFile(ec.GlobalConfigFile, data, 0644) + if err != nil { + return errors.Wrap(err, "writing global config file failed") + } + ec.Logger.Debugf("global config file written at '%s' with content '%v'", ec.GlobalConfigFile, string(data)) + // also show a notice about telemetry + ec.Logger.Info(StrTelemetryNotice) + } else if os.IsExist(err) || err == nil { + // file exists, verify contents + ec.Logger.Debug("global config file exisits, verifying contents") + data, err := ioutil.ReadFile(ec.GlobalConfigFile) + if err != nil { + return errors.Wrap(err, "reading global config file failed") + } + var gc GlobalConfig + err = json.Unmarshal(data, &gc) + if err != nil { + return errors.Wrap(err, "global config file not a valid json") + } + _, err = uuid.FromString(gc.UUID) + if err != nil { + ec.Logger.Debugf("invalid uuid '%s' in global config: %v", gc.UUID, err) + // create a new UUID + ec.Logger.Debug("global config file exists, but uuid is invalid, creating a new one...") + u, err := uuid.NewV4() + if err != nil { + return errors.Wrap(err, "failed to generate uuid") + } + gc.UUID = u.String() + data, err := json.Marshal(gc) + if err != nil { + return errors.Wrap(err, "cannot marshal json for config file") + } + err = ioutil.WriteFile(ec.GlobalConfigFile, data, 0644) + if err != nil { + return errors.Wrap(err, "writing global config file failed") + } + ec.Logger.Debugf("global config file written at '%s' with content '%v'", ec.GlobalConfigFile, string(data)) + } } - ec.GlobalConfigFile = filepath.Join(ec.GlobalConfigDir, GLOBAL_CONFIG_FILE_NAME) return nil } diff --git a/cli/cli_test.go b/cli/cli_test.go index 3b4a741fa3d..f3af7c025ff 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -22,10 +22,9 @@ func init() { func TestPrepare(t *testing.T) { logger, _ := test.NewNullLogger() - ec := &cli.ExecutionContext{ - Logger: logger, - Spinner: spinner.New(spinner.CharSets[7], 100*time.Millisecond), - } + ec := cli.NewExecutionContext() + ec.Logger = logger + ec.Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) ec.Spinner.Writer = &fake.FakeWriter{} err := ec.Prepare() if err != nil { @@ -47,16 +46,15 @@ func TestPrepare(t *testing.T) { t.Fatalf("global config file: expected $HOME/%s/%s, got %s", cli.GLOBAL_CONFIG_DIR_NAME, cli.GLOBAL_CONFIG_FILE_NAME, ec.GlobalConfigFile) } if ec.Config == nil { - t.Fatal("nil HasuraGraphQLConfig") + t.Fatal("got empty Config") } } func TestValidate(t *testing.T) { logger, _ := test.NewNullLogger() - ec := &cli.ExecutionContext{ - Logger: logger, - Spinner: spinner.New(spinner.CharSets[7], 100*time.Millisecond), - } + ec := cli.NewExecutionContext() + ec.Logger = logger + ec.Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) ec.Spinner.Writer = &fake.FakeWriter{} ec.ExecutionDirectory = filepath.Join(os.TempDir(), "hasura-gql-tests-"+strconv.Itoa(rand.Intn(1000))) ec.Viper = viper.New() diff --git a/cli/commands/console.go b/cli/commands/console.go index 6f6236180f2..982605618f3 100644 --- a/cli/commands/console.go +++ b/cli/commands/console.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/viper" ) +// NewConsoleCmd returns the console command func NewConsoleCmd(ec *cli.ExecutionContext) *cobra.Command { v := viper.New() opts := &consoleOptions{ @@ -100,13 +101,15 @@ func (o *consoleOptions) run() error { o.EC.Logger.Debugf("rendering console template [%s] with assets [%s]", consoleTemplateVersion, consoleAssetsVersion) consoleRouter, err := serveConsole(consoleTemplateVersion, o.StaticDir, gin.H{ - "apiHost": "http://" + o.Address, - "apiPort": o.APIPort, - "cliVersion": o.EC.Version.GetCLIVersion(), - "dataApiUrl": o.EC.Config.ParsedEndpoint.String(), - "dataApiVersion": "", - "accessKey": o.EC.Config.AccessKey, - "assetsVersion": consoleAssetsVersion, + "apiHost": "http://" + o.Address, + "apiPort": o.APIPort, + "cliVersion": o.EC.Version.GetCLIVersion(), + "dataApiUrl": o.EC.Config.ParsedEndpoint.String(), + "dataApiVersion": "", + "accessKey": o.EC.Config.AccessKey, + "assetsVersion": consoleAssetsVersion, + "enableTelemetry": o.EC.GlobalConfig.EnableTelemetry, + "cliUUID": o.EC.GlobalConfig.UUID, }) if err != nil { return errors.Wrap(err, "error serving console") @@ -147,6 +150,8 @@ func (o *consoleOptions) run() error { o.EC.Spinner.Stop() log.Infof("console running at: %s", consoleURL) + o.EC.Telemetry.Beam() + wg.Wait() return nil } diff --git a/cli/commands/console_test.go b/cli/commands/console_test.go index 34d089922e4..a9ae2d82331 100644 --- a/cli/commands/console_test.go +++ b/cli/commands/console_test.go @@ -13,23 +13,29 @@ import ( func TestConsoleCmd(t *testing.T) { logger, _ := test.NewNullLogger() + ec := cli.NewExecutionContext() + ec.Telemetry.Command = "TEST" + ec.Logger = logger + ec.Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) + ec.Config = &cli.HasuraGraphQLConfig{ + Endpoint: "http://localhost:8080", + AccessKey: "", + } + ec.Version = version.New() + err := ec.Prepare() + if err != nil { + t.Fatalf("prepare failed: %v", err) + } + opts := &consoleOptions{ - EC: &cli.ExecutionContext{ - Logger: logger, - Spinner: spinner.New(spinner.CharSets[7], 100*time.Millisecond), - Config: &cli.HasuraGraphQLConfig{ - Endpoint: "http://localhost:8080", - AccessKey: "", - }, - Version: version.New(), - }, + EC: ec, APIPort: "9693", ConsolePort: "9695", Address: "localhost", DontOpenBrowser: true, } opts.EC.Spinner.Writer = &fake.FakeWriter{} - err := opts.EC.Config.ParseEndpoint() + err = opts.EC.Config.ParseEndpoint() if err != nil { t.Fatal(err) } diff --git a/cli/commands/docs.go b/cli/commands/docs.go index 1513c078514..7584c16afbc 100644 --- a/cli/commands/docs.go +++ b/cli/commands/docs.go @@ -7,8 +7,10 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" + "github.com/spf13/viper" ) +// NewDocsCmd returns the docs command func NewDocsCmd(ec *cli.ExecutionContext) *cobra.Command { var docType, docDirectory string docsCmd := &cobra.Command{ @@ -16,7 +18,8 @@ func NewDocsCmd(ec *cli.ExecutionContext) *cobra.Command { Short: "Generate CLI docs in various formats", Hidden: true, SilenceUsage: true, - PreRunE: func(cmd *cobra.Command, args []string) (err error) { + PreRunE: func(cmd *cobra.Command, args []string) error { + ec.Viper = viper.New() return ec.Prepare() }, RunE: func(cmd *cobra.Command, args []string) (err error) { diff --git a/cli/commands/init.go b/cli/commands/init.go index d5e92d8ac73..129cff6690c 100644 --- a/cli/commands/init.go +++ b/cli/commands/init.go @@ -12,6 +12,7 @@ import ( "github.com/manifoldco/promptui" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/viper" ) const ( @@ -38,6 +39,7 @@ func NewInitCmd(ec *cli.ExecutionContext) *cobra.Command { # See https://docs.hasura.io/1.0/graphql/manual/migrations/index.html for more details`, SilenceUsage: true, PreRunE: func(cmd *cobra.Command, args []string) error { + ec.Viper = viper.New() return ec.Prepare() }, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cli/commands/init_test.go b/cli/commands/init_test.go index dcecb652957..8eea1d1336f 100644 --- a/cli/commands/init_test.go +++ b/cli/commands/init_test.go @@ -20,29 +20,20 @@ func init() { func TestInitCmd(t *testing.T) { logger, _ := test.NewNullLogger() + ec := cli.NewExecutionContext() + ec.Logger = logger + ec.Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) tt := []struct { name string opts *initOptions err error }{ {"only-init-dir", &initOptions{ - EC: &cli.ExecutionContext{ - Logger: logger, - Spinner: spinner.New(spinner.CharSets[7], 100*time.Millisecond), - }, + EC: ec, Endpoint: "", AccessKey: "", InitDir: filepath.Join(os.TempDir(), "hasura-cli-test-"+strconv.Itoa(rand.Intn(1000))), }, nil}, - {"with-endpoint-flag", &initOptions{ - EC: &cli.ExecutionContext{ - Logger: logger, - Spinner: spinner.New(spinner.CharSets[7], 100*time.Millisecond), - }, - Endpoint: "https://localhost:8080", - AccessKey: "", - InitDir: filepath.Join(os.TempDir(), "hasura-cli-test-"+strconv.Itoa(rand.Intn(1000))), - }, nil}, } for _, tc := range tt { diff --git a/cli/commands/root.go b/cli/commands/root.go index f1c53c90a27..21df110cd2f 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -7,16 +7,22 @@ import ( "github.com/spf13/cobra" ) +// ec is the Execution Context for the current run. +var ec *cli.ExecutionContext + // rootCmd is the main "hasura" command var rootCmd = &cobra.Command{ Use: "hasura", Short: "Hasura GraphQL Engine command line tool", SilenceUsage: true, SilenceErrors: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + ec.Telemetry.Command = cmd.CommandPath() + }, } func init() { - ec := &cli.ExecutionContext{} + ec = cli.NewExecutionContext() rootCmd.AddCommand( NewInitCmd(ec), NewConsoleCmd(ec), @@ -32,5 +38,13 @@ func init() { // Execute executes the command and returns the error func Execute() error { - return rootCmd.Execute() + err := rootCmd.Execute() + if err != nil { + ec.Telemetry.IsError = true + } + ec.Telemetry.Beam() + if ec.Spinner != nil { + ec.Spinner.Stop() + } + return err } diff --git a/cli/commands/version.go b/cli/commands/version.go index f18b7928a15..0243b981924 100644 --- a/cli/commands/version.go +++ b/cli/commands/version.go @@ -13,11 +13,11 @@ func NewVersionCmd(ec *cli.ExecutionContext) *cobra.Command { Short: "Print the CLI version", SilenceUsage: true, PreRunE: func(cmd *cobra.Command, args []string) error { + ec.Viper = viper.New() return ec.Prepare() }, RunE: func(cmd *cobra.Command, args []string) error { ec.Logger.WithField("version", ec.Version.GetCLIVersion()).Info("hasura cli") - ec.Viper = viper.New() err := ec.Validate() if err == nil { ec.Logger. diff --git a/cli/get.sh b/cli/get.sh new file mode 100755 index 00000000000..379ae29a7db --- /dev/null +++ b/cli/get.sh @@ -0,0 +1,117 @@ +# adapted from https://github.com/openfaas/faas-cli/blob/master/get.sh +version=$(curl -s -H 'Content-Type: text/plain' https://releases.hasura.io/graphql-engine?agent=cli-get.sh) +if [ ! $version ]; then + echo "Failed while attempting to install hasura graphql-engine cli. Please manually install:" + echo "" + echo "1. Open your web browser and go to https://github.com/hasura/graphql-engine/releases" + echo "2. Download the cli from latest release for your platform. Call it 'hasura'." + echo "3. chmod +x ./hasura" + echo "4. mv ./hasura /usr/local/bin" + exit 1 +fi + +hasCli() { + + has=$(which hasura) + + if [ "$?" = "0" ]; then + echo + echo "You already have the hasura cli!" + export n=1 + echo "Overwriting in $n seconds.. Press Control+C to cancel." + echo + sleep $n + fi + + hasCurl=$(which curl) + if [ "$?" = "1" ]; then + echo "You need curl to use this script." + exit 1 + fi +} + + +getPackage() { + uname=$(uname) + userid=$(id -u) + + suffix="" + case $uname in + "Darwin") + suffix="-darwin-amd64" + ;; + "Linux") + arch=$(uname -m) + echo $arch + case $arch in + "amd64" | "x86_64") + suffix="-linux-amd4" + ;; + esac + case $arch in + "aarch64") + suffix="-linux-arm64" + ;; + esac + case $arch in + "armv6l" | "armv7l") + suffix="-linux-armhf" + ;; + esac + ;; + esac + + targetFile="/tmp/cli-hasura$suffix" + + if [ "$userid" != "0" ]; then + targetFile="$(pwd)/cli-hasura$suffix" + fi + + if [ -e $targetFile ]; then + rm $targetFile + fi + + url=https://github.com/hasura/graphql-engine/releases/download/$version/cli-hasura$suffix + echo "Downloading package $url as $targetFile" + + curl -sSL $url --output $targetFile + + if [ "$?" = "0" ]; then + + chmod +x $targetFile + + echo "Download complete." + + if [ "$userid" != "0" ]; then + + echo + echo "=========================================================" + echo "== As the script was run as a non-root user the ==" + echo "== following command may need to be run manually ==" + echo "=========================================================" + echo + echo " sudo cp cli-hasura$suffix /usr/local/bin/hasura" + echo + + else + + echo + echo "Running as root - Attempting to move hasura cli to /usr/local/bin" + + mv $targetFile /usr/local/bin/hasura + + if [ "$?" = "0" ]; then + echo "New version of hasura cli installed to /usr/local/bin" + fi + + if [ -e $targetFile ]; then + rm $targetFile + fi + + hasura version + fi + fi +} + +hasCli +getPackage \ No newline at end of file diff --git a/cli/telemetry/telemetry.go b/cli/telemetry/telemetry.go new file mode 100644 index 00000000000..970e802b0aa --- /dev/null +++ b/cli/telemetry/telemetry.go @@ -0,0 +1,120 @@ +package telemetry + +import ( + "runtime" + "sync" + "time" + + "github.com/hasura/graphql-engine/cli/version" + "github.com/parnurzeal/gorequest" + "github.com/sirupsen/logrus" +) + +// Waiter waits for telemetry ops to complete, if required +var Waiter sync.WaitGroup + +// Endpoint is where telemetry data is sent. +const Endpoint = "https://telemetry.hasura.io/v1/http" + +// Topic is the name under which telemetry is sent. +var Topic = "cli_test" + +func init() { + var v = version.New() + if v.CLISemver != nil { + Topic = "cli" + } +} + +type requestPayload struct { + Topic string `json:"topic"` + Data `json:"data"` +} + +// Data holds all info collected and transmitted +type Data struct { + // UUID used for telemetry, generated on first run. + UUID string `json:"uuid"` + + // UUID obtained from server. + ServerUUID string `json:"server_uuid"` + + // Unique id for the current execution. + ExecutionID string `json:"execution_id"` + + // OS platform and architecture. + OSPlatform string `json:"os_platform"` + OSArch string `json:"os_arch"` + + // Current cli version. + Version string `json:"version"` + + // Current Server version. + ServerVersion string `json:"server_version"` + + // Command being executed. + Command string `json:"command"` + + // Indicates whether the execution resulted in an error or not. + IsError bool `json:"is_error"` + + // Any additional payload information. + Payload map[string]interface{} `json:"payload"` + + // Additional objects - mandatory + Logger *logrus.Logger `json:"-"` + + // IsBeamed indicates if this data is already beamed or not. + IsBeamed bool `json:"-"` + + // CanBeam indicates if data can be beamed or not, e.g. disabled telemetry. + CanBeam bool `json:"-"` +} + +// BuildEvent returns a Data object which represent a telemetry event +func BuildEvent() *Data { + return &Data{ + OSPlatform: runtime.GOOS, + OSArch: runtime.GOARCH, + CanBeam: true, + } +} + +// Beam the telemetry data +func (d *Data) Beam() { + // to be on the safe side, create a new logger if a logger + // is not passed + if d.Logger == nil { + d.Logger = logrus.New() + } + if !d.CanBeam { + d.Logger.Debugf("telemetry: disabled, not beaming any data") + return + } + if !d.IsBeamed { + beam(d, d.Logger) + } else { + d.Logger.Debugf("telemetry: data already beamed") + } +} + +func beam(d *Data, log *logrus.Logger) { + d.IsBeamed = true + p := requestPayload{ + Topic: Topic, + Data: *d, + } + tick := time.Now() + _, _, err := gorequest.New(). + Post(Endpoint). + Timeout(2 * time.Second). + Send(p). + End() + if err != nil { + log.Debugf("telemetry: beaming payload failed: %v", err) + } else { + tock := time.Now() + delta := tock.Sub(tick) + log.WithField("isError", d.IsError).WithField("time", delta.String()).Debug("telemetry: beamed") + } +} diff --git a/cli/telemetry/telemetry_test.go b/cli/telemetry/telemetry_test.go new file mode 100644 index 00000000000..d9e67257317 --- /dev/null +++ b/cli/telemetry/telemetry_test.go @@ -0,0 +1,16 @@ +package telemetry_test + +import ( + "testing" + + "github.com/hasura/graphql-engine/cli/telemetry" + "github.com/hasura/graphql-engine/cli/version" +) + +func TestBeam(t *testing.T) { + tm := telemetry.BuildEvent() + tm.Version = version.BuildVersion + tm.Command = "TEST" + tm.CanBeam = true + tm.Beam() +} diff --git a/cli/util/server.go b/cli/util/server.go new file mode 100644 index 00000000000..edebd434669 --- /dev/null +++ b/cli/util/server.go @@ -0,0 +1,58 @@ +package util + +import ( + "github.com/sirupsen/logrus" + + "github.com/Masterminds/semver" + "github.com/parnurzeal/gorequest" +) + +// ServerState is the state of Hasura stored on the server. +type ServerState struct { + UUID string + CLIState map[string]interface{} +} + +type hdbVersion struct { + UUID string `json:"hasura_uuid"` + CLIState map[string]interface{} `json:"cli_state"` +} + +// GetServerState queries a server for the state. +func GetServerState(endpoint, accessKey string, serverVersion *semver.Version, log *logrus.Logger) *ServerState { + state := &ServerState{ + UUID: "00000000-0000-0000-0000-000000000000", + } + payload := `{ + "type": "select", + "args": { + "table": { + "schema": "hdb_catalog", + "name": "hdb_version" + }, + "columns": [ + "hasura_uuid", + "cli_state" + ] + } + }` + req := gorequest.New() + req = req.Post(endpoint + "/v1/query").Send(payload) + req.Set("X-Hasura-Access-Key", accessKey) + + var r []hdbVersion + _, _, errs := req.EndStruct(&r) + if len(errs) != 0 { + log.Debugf("server state: errors: %v", errs) + return state + } + + if len(r) != 1 { + log.Debugf("invalid response: %v", r) + return state + } + + state.UUID = r[0].UUID + state.CLIState = r[0].CLIState + return state +} diff --git a/cli/version/compatibility.go b/cli/version/compatibility.go index e08928c5a66..cd0ba8b0e69 100644 --- a/cli/version/compatibility.go +++ b/cli/version/compatibility.go @@ -6,12 +6,17 @@ const ( noServerVersion = "server with no version treated as pre-release build" noCLIVersion = "cli version is empty, indicates a broken build" untaggedCLI = "untagged cli build can work with tagged server build" + devCLI = "dev version of cli, compatible with all servers" ) // CheckCLIServerCompatibility compares server and cli for compatibility, // subject to certain conditions. compatible boolean is returned along with // a message which states the reason for the result. func (v *Version) CheckCLIServerCompatibility() (compatible bool, reason string) { + // mark dev builds as compatible + if v.CLI == "dev" { + return true, devCLI + } // empty cli version if v.CLI == "" { return false, noCLIVersion diff --git a/console/src/Endpoints.js b/console/src/Endpoints.js index 9c832ecbf81..9b729ba46b7 100644 --- a/console/src/Endpoints.js +++ b/console/src/Endpoints.js @@ -17,6 +17,7 @@ const Endpoints = { hasuractlMigrate: `${hasuractlUrl}/apis/migrate`, hasuractlMetadata: `${hasuractlUrl}/apis/metadata`, hasuractlMigrateSettings: `${hasuractlUrl}/apis/migrate/settings`, + telemetryServer: 'wss://telemetry.hasura.io/v1/ws', }; const globalCookiePolicy = 'omit'; diff --git a/console/src/Globals.js b/console/src/Globals.js index 5a33109441a..90bab995a40 100644 --- a/console/src/Globals.js +++ b/console/src/Globals.js @@ -32,6 +32,9 @@ const globals = { ? 'server' : window.__env.consoleMode, urlPrefix: checkExtraSlashes(window.__env.urlPrefix), + enableTelemetry: window.__env.enableTelemetry, + telemetryTopic: + window.__env.nodeEnv !== 'development' ? 'console' : 'console_test', }; // set defaults diff --git a/console/src/client.js b/console/src/client.js index 913aa36b651..4cc6a1c7e71 100755 --- a/console/src/client.js +++ b/console/src/client.js @@ -19,13 +19,97 @@ import getRoutes from './routes'; import reducer from './reducer'; import globals from './Globals'; +import Endpoints from './Endpoints'; +import { filterEventsBlockList, sanitiseUrl } from './telemetryFilter'; + +const analyticsUrl = Endpoints.telemetryServer; +let analyticsConnection; +const { consoleMode, enableTelemetry, cliUUID } = window.__env; +const telemetryEnabled = + enableTelemetry !== undefined && enableTelemetry === true; +if (telemetryEnabled) { + try { + analyticsConnection = new WebSocket(analyticsUrl); + } catch (error) { + console.error(error); + } +} + +const onError = error => { + console.log('WebSocket Error for Events' + error); +}; + +const onClose = () => { + try { + analyticsConnection = new WebSocket(analyticsUrl); + } catch (error) { + console.error(error); + } + analyticsConnection.onclose = onClose(); + analyticsConnection.onerror = onError(); +}; + +function analyticsLogger({ getState }) { + return next => action => { + // Call the next dispatch method in the middleware chain. + const returnValue = next(action); + // check if analytics tracking is enabled + if (telemetryEnabled) { + const serverVersion = getState().main.serverVersion; + const actionType = action.type; + const url = sanitiseUrl(window.location.pathname); + const reqBody = { + server_version: serverVersion, + event_type: actionType, + url, + console_mode: consoleMode, + cli_uuid: cliUUID, + server_uuid: getState().telemetry.hasura_uuid, + }; + + let isLocationType = false; + if (actionType === '@@router/LOCATION_CHANGE') { + isLocationType = true; + } + // filter events + if (!filterEventsBlockList.includes(actionType)) { + if ( + analyticsConnection && + analyticsConnection.readyState === analyticsConnection.OPEN + ) { + // When the connection is open, send data to the server + if (isLocationType) { + // capture page views + const payload = action.payload; + reqBody.url = sanitiseUrl(payload.pathname); + } + analyticsConnection.send( + JSON.stringify({ data: reqBody, topic: globals.telemetryTopic }) + ); // Send the data + // check for possible error events and store more data? + } else { + // retry websocket connection + // analyticsConnection = new WebSocket(analyticsUrl); + } + } + } + // This will likely be the action itself, unless + // a middleware further in chain changed it. + return returnValue; + }; +} // Create the store let _finalCreateStore; if (__DEVELOPMENT__) { const tools = [ - applyMiddleware(thunk, routerMiddleware(browserHistory), createLogger()), + applyMiddleware( + thunk, + routerMiddleware(browserHistory), + createLogger(), + analyticsLogger + ), require('redux-devtools').persistState( window.location.href.match(/[?&]debug_session=([^&]+)\b/) ), @@ -36,7 +120,7 @@ if (__DEVELOPMENT__) { _finalCreateStore = compose(...tools)(createStore); } else { _finalCreateStore = compose( - applyMiddleware(thunk, routerMiddleware(browserHistory)) + applyMiddleware(thunk, routerMiddleware(browserHistory), analyticsLogger) )(createStore); } diff --git a/console/src/components/App/App.js b/console/src/components/App/App.js index a57f0848a77..8ffe4634a87 100644 --- a/console/src/components/App/App.js +++ b/console/src/components/App/App.js @@ -9,6 +9,8 @@ import { NOTIF_EXPANDED } from './Actions'; import AceEditor from 'react-ace'; import 'brace/mode/json'; import ErrorBoundary from './ErrorBoundary'; +import { telemetryNotificationShown } from '../../telemetry/Actions'; +import { showTelemetryNotification } from '../../telemetry/Notifications'; class App extends Component { componentDidMount() { @@ -36,6 +38,8 @@ class App extends Component { connectionFailed, isNotifExpanded, notifMsg, + telemetry, + dispatch, } = this.props; if (requestError && error) { @@ -77,6 +81,13 @@ class App extends Component { ); } + if (telemetry.console_opts) { + if (!telemetry.console_opts.telemetryNotificationShown) { + dispatch(showTelemetryNotification()); + dispatch(telemetryNotificationShown()); + } + } + return (
@@ -153,6 +164,7 @@ const mapStateToProps = state => { return { ...state.progressBar, notifications: state.notifications, + telemetry: state.telemetry, }; }; diff --git a/console/src/components/Main/Main.js b/console/src/components/Main/Main.js index 128b8b49620..8d459e93abd 100644 --- a/console/src/components/Main/Main.js +++ b/console/src/components/Main/Main.js @@ -7,6 +7,7 @@ import * as tooltip from './Tooltips'; import 'react-toggle/style.css'; import Spinner from '../Common/Spinner/Spinner'; import { loadServerVersion, checkServerUpdates } from './Actions'; +import { loadConsoleOpts } from '../../telemetry/Actions.js'; import './NotificationOverrides.css'; import semverCheck from '../../helpers/semver'; @@ -35,6 +36,7 @@ class Main extends React.Component { .querySelector('body') .addEventListener('click', this.handleBodyClick); dispatch(loadServerVersion()).then(() => { + dispatch(loadConsoleOpts()); dispatch(checkServerUpdates()).then(() => { let isUpdateAvailable = false; try { @@ -50,9 +52,12 @@ class Main extends React.Component { ); if (isClosedBefore === 'true') { isUpdateAvailable = false; - this.setState({ showBannerNotification: false }); + this.setState({ ...this.state, showBannerNotification: false }); } else { - this.setState({ showBannerNotification: isUpdateAvailable }); + this.setState({ + ...this.state, + showBannerNotification: isUpdateAvailable, + }); } } catch (e) { console.error(e); @@ -73,7 +78,7 @@ class Main extends React.Component { checkEventsTab() { const showEvents = semverCheck('eventsTab', this.props.serverVersion); if (showEvents) { - this.setState({ showEvents: true }); + this.setState({ ...this.state, showEvents: true }); } return Promise.resolve(); } @@ -107,7 +112,7 @@ class Main extends React.Component { latestServerVersion + '_BANNER_NOTIFICATION_CLOSED', 'true' ); - this.setState({ showBannerNotification: false }); + this.setState({ ...this.state, showBannerNotification: false }); } render() { diff --git a/console/src/components/Main/Main.scss b/console/src/components/Main/Main.scss index 3f56211ecfb..a3ce45cb7e5 100644 --- a/console/src/components/Main/Main.scss +++ b/console/src/components/Main/Main.scss @@ -1116,3 +1116,10 @@ border: 0; } } +.telemetryNotification { + background-color: red; + padding: 10px; + position: fixed; + bottom: 50px; + right: 0px; +} \ No newline at end of file diff --git a/console/src/components/Main/State.js b/console/src/components/Main/State.js index c0b6c86911c..44f77f7804f 100644 --- a/console/src/components/Main/State.js +++ b/console/src/components/Main/State.js @@ -9,6 +9,7 @@ const defaultState = { loginError: false, serverVersion: null, latestServerVersion: null, + telemetryEnabled: true, }; export default defaultState; diff --git a/console/src/helpers/Html.js b/console/src/helpers/Html.js index d8ca7fc0357..047ff8036bd 100644 --- a/console/src/helpers/Html.js +++ b/console/src/helpers/Html.js @@ -47,7 +47,8 @@ export default class Html extends Component { isAccessKeySet: ${process.env.IS_ACCESS_KEY_SET}, consoleMode: '${process.env.CONSOLE_MODE}', nodeEnv: '${process.env.NODE_ENV}', - urlPrefix: '${process.env.URL_PREFIX}' + urlPrefix: '${process.env.URL_PREFIX}', + enableTelemetry: ${process.env.ENABLE_TELEMETRY} };`, }} /> diff --git a/console/src/reducer.js b/console/src/reducer.js index 0efc0d83c7c..f0b670ec44f 100644 --- a/console/src/reducer.js +++ b/console/src/reducer.js @@ -6,6 +6,7 @@ import { customResolverReducer } from './components/Services/CustomResolver'; import mainReducer from './components/Main/Actions'; import apiExplorerReducer from 'components/ApiExplorer/Actions'; import progressBarReducer from 'components/App/Actions'; +import telemetryReducer from './telemetry/Actions'; import { reducer as notifications } from 'react-notification-system-redux'; @@ -17,6 +18,7 @@ const reducer = combineReducers({ main: mainReducer, routing: routerReducer, customResolverData: customResolverReducer, + telemetry: telemetryReducer, notifications, }); diff --git a/console/src/telemetry/Actions.js b/console/src/telemetry/Actions.js new file mode 100644 index 00000000000..84b3d2c3fc8 --- /dev/null +++ b/console/src/telemetry/Actions.js @@ -0,0 +1,122 @@ +import Endpoints, { globalCookiePolicy } from '../Endpoints'; +import requestAction from '../utils/requestAction'; +import dataHeaders from '../components/Services/Data/Common/Headers'; +import defaultTelemetryState from './State'; + +const SET_CONSOLE_OPTS = 'Telemetry/SET_CONSOLE_OPTS'; +const SET_NOTIFICATION_SHOWN = 'Telemetry/SET_NOTIFICATION_SHOWN'; +const SET_HASURA_UUID = 'Telemetry/SET_HASURA_UUID'; +const SET_TELEMETRY_DISABLED = 'Telemetr/SET_TELEMETRY_DISABLED'; + +const telemetryNotificationShown = () => dispatch => { + dispatch({ type: SET_NOTIFICATION_SHOWN }); +}; + +const setNotificationShownInDB = () => (dispatch, getState) => { + const url = Endpoints.getSchema; + const uuid = getState().telemetry.hasura_uuid; + const options = { + credentials: globalCookiePolicy, + method: 'POST', + headers: dataHeaders(getState), + body: JSON.stringify({ + type: 'run_sql', + args: { + sql: `update hdb_catalog.hdb_version set console_state = console_state || jsonb_build_object('telemetryNotificationShown', true) where hasura_uuid='${uuid}';`, + }, + }), + }; + return dispatch(requestAction(url, options)).then( + data => { + console.log( + 'Updated telemetry notification status in db' + JSON.stringify(data) + ); + }, + error => { + console.error( + 'Failed to update telemetry notification status in db' + + JSON.stringify(error) + ); + } + ); +}; + +const loadConsoleOpts = () => { + return (dispatch, getState) => { + if (window.__env.enableTelemetry === undefined) { + return dispatch({ type: SET_TELEMETRY_DISABLED }); + } + const url = Endpoints.getSchema; + const options = { + credentials: globalCookiePolicy, + method: 'POST', + headers: dataHeaders(getState), + body: JSON.stringify({ + type: 'select', + args: { + table: { + name: 'hdb_version', + schema: 'hdb_catalog', + }, + columns: ['hasura_uuid', 'console_state'], + }, + }), + }; + return dispatch(requestAction(url, options)).then( + data => { + if (data.length !== 0) { + dispatch({ + type: SET_HASURA_UUID, + data: data[0].hasura_uuid, + }); + dispatch({ + type: SET_CONSOLE_OPTS, + data: data[0].console_state, + }); + } + }, + error => { + console.error( + 'Failed to load telemetry misc options' + JSON.stringify(error) + ); + } + ); + }; +}; + +const telemetryReducer = (state = defaultTelemetryState, action) => { + switch (action.type) { + case SET_CONSOLE_OPTS: + return { + ...state, + console_opts: { + ...action.data, + telemetryNotificationShown: action.data.telemetryNotificationShown + ? true + : false, + }, + }; + case SET_NOTIFICATION_SHOWN: + return { + ...state, + console_opts: { + ...state.console_opts, + telemetryNotificationShown: true, + }, + }; + case SET_HASURA_UUID: + return { + ...state, + hasura_uuid: action.data, + }; + default: + return state; + } +}; + +export default telemetryReducer; +export { + loadConsoleOpts, + telemetryNotificationShown, + setNotificationShownInDB, +}; diff --git a/console/src/telemetry/Notifications.js b/console/src/telemetry/Notifications.js new file mode 100644 index 00000000000..5a5065fb14a --- /dev/null +++ b/console/src/telemetry/Notifications.js @@ -0,0 +1,40 @@ +import React from 'react'; +import Notifications from 'react-notification-system-redux'; +import { setNotificationShownInDB } from './Actions'; + +const onRemove = () => { + return dispatch => { + dispatch(setNotificationShownInDB()); + }; +}; + +const showTelemetryNotification = () => { + return dispatch => { + dispatch( + Notifications.show({ + position: 'tr', + autoDismiss: 10, + level: 'info', + title: 'Telemetry', + children: ( +
+ Help us improve Hasura! The console collects anonymized usage stats + which allows us to keep improving Hasura at warp speed. + + {' '} + Click here + {' '} + to read more or to opt-out. +
+ ), + onRemove: () => dispatch(onRemove()), + }) + ); + }; +}; + +export { showTelemetryNotification }; diff --git a/console/src/telemetry/State.js b/console/src/telemetry/State.js new file mode 100644 index 00000000000..34ecd4e9d64 --- /dev/null +++ b/console/src/telemetry/State.js @@ -0,0 +1,6 @@ +const defaultTelemetryState = { + console_opts: null, + hasura_uuid: null, +}; + +export default defaultTelemetryState; diff --git a/console/src/telemetryFilter.js b/console/src/telemetryFilter.js new file mode 100644 index 00000000000..8073c44ed56 --- /dev/null +++ b/console/src/telemetryFilter.js @@ -0,0 +1,61 @@ +import globals from './Globals'; + +const filterEventsBlockList = [ + 'App/ONGOING_REQUEST', + 'App/DONE_REQUEST', + 'App/FAILED_REQUEST', + 'App/ERROR_REQUEST', +]; + +const filterPayloadAllowList = []; + +const DATA_PATH = '/data'; +const API_EXPLORER_PATH = '/api-explorer'; +const REMOTE_SCHEMAS_PATH = '/remote-schemas'; +const EVENTS_PATH = '/events'; + +const dataHandler = path => { + return ( + DATA_PATH + + path + .replace(/\/schema\/(\w+)(\/)?/, '/schema/SCHEMA_NAME$2') + .replace(/(\/schema\/.*)\/tables\/(\w*)(\/.*)?/, '$1/tables/TABLE_NAME$3') + ); +}; + +const apiExplorerHandler = () => { + return API_EXPLORER_PATH; +}; + +const remoteSchemasHandler = path => { + return ( + REMOTE_SCHEMAS_PATH + + path.replace(/(\/manage\/).*(\/\w+.*)$/, '$1REMOTE_SCHEMA_NAME$2') + ); +}; + +const eventsHandler = path => { + return ( + EVENTS_PATH + + path.replace(/(\/manage\/triggers\/).*(\/\w+.*)$/, '$1TRIGGER_NAME$2') + ); +}; + +const sanitiseUrl = path => { + path = path.replace(new RegExp(globals.urlPrefix, 'g'), ''); + if (path.indexOf(DATA_PATH) === 0) { + return dataHandler(path.slice(DATA_PATH.length)); + } + if (path.indexOf(API_EXPLORER_PATH) === 0) { + return apiExplorerHandler(path.slice(API_EXPLORER_PATH.length)); + } + if (path.indexOf(REMOTE_SCHEMAS_PATH) === 0) { + return remoteSchemasHandler(path.slice(REMOTE_SCHEMAS_PATH.length)); + } + if (path.indexOf(EVENTS_PATH) === 0) { + return eventsHandler(path.slice(EVENTS_PATH.length)); + } + return '/'; +}; + +export { filterEventsBlockList, filterPayloadAllowList, sanitiseUrl }; diff --git a/docs/graphql/manual/deployment/graphql-engine-flags/reference.rst b/docs/graphql/manual/deployment/graphql-engine-flags/reference.rst index 2874a376e44..b9b44553e2b 100644 --- a/docs/graphql/manual/deployment/graphql-engine-flags/reference.rst +++ b/docs/graphql/manual/deployment/graphql-engine-flags/reference.rst @@ -81,6 +81,8 @@ For ``serve`` subcommand these are the flags available --use-prepared-statements Use prepared statements for SQL queries (default: true) + --enable-telemetry Enable anonymous telemetry (default: true) + Default environment variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -147,5 +149,7 @@ These are the environment variables which are available: HASURA_GRAPHQL_ENABLE_CONSOLE Enable API console. It is served at '/' and '/console' + HASURA_GRAPHQL_ENABLE_TELEMETRY Enable anonymous telemetry (default: true) + HASURA_GRAPHQL_USE_PREPARED_STATEMENTS Use prepared statements for SQL queries (default: true) diff --git a/docs/graphql/manual/guides/index.rst b/docs/graphql/manual/guides/index.rst index c6c83da25b2..2e5ce43e598 100644 --- a/docs/graphql/manual/guides/index.rst +++ b/docs/graphql/manual/guides/index.rst @@ -50,6 +50,10 @@ Postgres Auditing - :doc:`Auditing tables ` +Telemetry +--------- + +- :doc:`Guide on telemetry and instructions to opt-out ` .. toctree:: :maxdepth: 1 @@ -61,3 +65,4 @@ Postgres Auditing Integration/migration tutorials Integrating with monitoring frameworks Auditing tables + Telemetry diff --git a/docs/graphql/manual/guides/telemetry.rst b/docs/graphql/manual/guides/telemetry.rst new file mode 100644 index 00000000000..ce723eb98aa --- /dev/null +++ b/docs/graphql/manual/guides/telemetry.rst @@ -0,0 +1,148 @@ +.. _telemetry: + +Telemetry Guide/FAQ +=================== + +.. contents:: Table of contents + :backlinks: none + :depth: 1 + :local: + +The Hasura GraphQL Engine collects anonymous telemetry data that helps the +Hasura team in understanding how the product is being used and in deciding +what to focus on next. + +The data collected is minimal and, since there is no *sign-in* associated with +the GraphQL Engine, it **cannot be used to uniquely identify any user**. +Furthermore, data collected is strictly statistical in nature and +**no proprietary information is collected** (*please see the next section*). + +As a growing community, we greatly appreciate the telemetry data users +send to us, as it is very valuable in making GraphQL Engine a better product +for everyone. If you are worried about privacy, you can choose to disable +sending telemetry as described :ref:`here `. + +.. note:: + + Access to collected data is strictly limited to the Hasura team and not shared with 3rd parties. + +What data are collected? +------------------------ + +Server +~~~~~~ + +The server periodically sends the number of tables, views, relationships, +permission rules, event triggers and remote schemas tracked by GraphQL Engine, +along with randomly generated UUID per database and per instance. The +server version is also sent. + +Here is a sample row from the telemetry database: + +.. code-block:: json + + { + "id": 12, + "timestamp": "2019-01-21T19:43:33.63838+00:00", + "db_uid": "dddff371-dab2-450f-9969-235bca66dab1", + "instance_uid": "6799360d-a431-40c5-9f68-24592a9f07df", + "version": "v1.0.0-alpha35", + "metrics": { + "views": 1, + "tables": 2, + "permissions": { + "roles": 1, + "delete": 2, + "insert": 1, + "select": 2, + "update": 2 + }, + "relationships": { + "auto": 2, + "manual": 0 + }, + "event_triggers": 0, + "remote_schemas": 1 + } + } + + +Console +~~~~~~~ + +The console is a React-Redux UI. Redux action names along with anonymized +route names are sent without any identifiable information or payload. Console +also records the UUID of the server/CLI that it is connected to. + +Here is a sample: + +.. code-block:: json + + { + "id": 902, + "timestamp": "2019-01-21T10:00:23.849202+00:00", + "url": "/data/schema/SCHEMA_NAME/tables/TABLE_NAME/modify", + "event_type": "ModifyTable/RESET", + "console_mode": "server", + "server_uuid": "79485a57-fca5-40f3-a31b-78c0d211314b", + "server_version": "v1.0.0-alpha35", + "cli_uuid": null + } + + +CLI +~~~ + +The CLI collects each execution event, along with a randomly generated UUID. +The execution event contains the command name, timestamp and whether the +execution resulted in an error or not. **Error messages, arguments and flags +are not recorded**. CLI also collects the server version and UUID that it +is talking to. The operating system platform and architecture is also +noted along with the CLI version. + +Sample data: + +.. code-block:: json + + { + "id": 115, + "timestamp": "2019-01-21T11:36:07.86783+00:00", + "uuid": "e462ce20-42dd-40fd-9549-edfb92f80455", + "execution_id": "ddfa9c33-0693-457d-9026-c7f456c43322", + "version": "v0.4.27", + "command": "hasura version", + "is_error": false, + "os_platform": "linux", + "os_arch": "amd64", + "server_uuid": "a4d66fb2-f88d-457b-8db1-ea7a0b57921d", + "server_version": "v1.0.0-alpha35", + "payload": null + } + +Where is the data sent? +----------------------- + +The data is sent to Hasura's servers addressed by ``telemetry.hasura.io``. + +.. _telemetry_optout: + +How do I turn off telemetry (opt-out)? +-------------------------------------- + +You can turn off telemetry on the server and on the console hosted by server +by setting the following environment variable on the server or by using +the flag ``--enable-telemetry=false``: + +.. code-block:: bash + + HASURA_GRAPHQL_ENABLE_TELEMETRY=false + +In order to turn off telemetry on CLI and on the console served by CLI, +you can set the same environment varibale on the machine running CLI. +You can also set ``"enable_telemetry": false`` in the JSON file created +by the CLI at ``~/.hasura/.config.json`` to perisist the setting. + +Privacy Policy +-------------- + +You can check out our privacy policy `here `_. \ No newline at end of file diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 148dca2d119..77deaab336a 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -148,6 +148,7 @@ library , Hasura.Server.Utils , Hasura.Server.Version , Hasura.Server.CheckUpdates + , Hasura.Server.Telemetry , Hasura.RQL.Types , Hasura.RQL.Instances , Hasura.RQL.Types.SchemaCache diff --git a/server/src-exec/Main.hs b/server/src-exec/Main.hs index c08cf3b1d08..b834b9bf839 100644 --- a/server/src-exec/Main.hs +++ b/server/src-exec/Main.hs @@ -31,6 +31,7 @@ import Hasura.Server.Auth import Hasura.Server.CheckUpdates (checkForUpdates) import Hasura.Server.Init import Hasura.Server.Query (peelRun) +import Hasura.Server.Telemetry import Hasura.Server.Version (currentVersion) import qualified Database.PG.Query as Q @@ -71,6 +72,7 @@ parseHGECommand = <*> parseUnAuthRole <*> parseCorsConfig <*> parseEnableConsole + <*> parseEnableTelemetry parseArgs :: IO HGEOptions parseArgs = do @@ -100,8 +102,8 @@ main = do loggerCtx <- mkLoggerCtx $ defaultLoggerSettings True let logger = mkLogger loggerCtx case hgeCmd of - HCServe so@(ServeOptions port host cp isoL mAccessKey mAuthHook - mJwtSecret mUnAuthRole corsCfg enableConsole) -> do + HCServe so@(ServeOptions port host cp isoL mAccessKey mAuthHook mJwtSecret + mUnAuthRole corsCfg enableConsole enableTelemetry) -> do -- log serve options unLogger logger $ serveOptsToLog so hloggerCtx <- mkLoggerCtx $ defaultLoggerSettings False @@ -114,22 +116,19 @@ main = do ci <- procConnInfo rci -- log postgres connection info unLogger logger $ connInfoToLog ci + -- safe init catalog - initialise logger ci httpManager - -- migrate catalog if necessary - migrate logger ci httpManager + initRes <- initialise logger ci httpManager + -- prepare event triggers data prepareEvents logger ci pool <- Q.initPGPool ci cp (app, cacheRef) <- mkWaiApp isoL loggerCtx pool httpManager - am corsCfg enableConsole + am corsCfg enableConsole enableTelemetry let warpSettings = Warp.setPort port $ Warp.setHost host Warp.defaultSettings - -- start a background thread to check for updates - void $ C.forkIO $ checkForUpdates loggerCtx httpManager - maxEvThrds <- getFromEnv defaultMaxEventThreads "HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE" evFetchMilliSec <- getFromEnv defaultFetchIntervalMilliSec "HASURA_GRAPHQL_EVENTS_FETCH_INTERVAL" logEnvHeaders <- getFromEnv False "LOG_HEADERS_FROM_ENV" @@ -141,6 +140,14 @@ main = do mkGenericStrLog "event_triggers" "starting workers" void $ C.forkIO $ processEventQueue hloggerCtx logEnvHeaders httpSession pool cacheRef eventEngineCtx + -- start a background thread to check for updates + void $ C.forkIO $ checkForUpdates loggerCtx httpManager + + -- start a background thread for telemetry + when enableTelemetry $ do + unLogger logger $ mkGenericStrLog "telemetry" telemetryNotice + void $ C.forkIO $ runTelemetry logger httpManager cacheRef initRes + unLogger logger $ mkGenericStrLog "server" "starting API server" Warp.runSettings warpSettings app @@ -163,6 +170,7 @@ main = do HCVersion -> putStrLn $ "Hasura GraphQL Engine: " ++ T.unpack currentVersion where + runTx :: Q.ConnInfo -> Q.TxE QErr a -> IO (Either QErr a) runTx ci tx = do pool <- getMinimalPool ci @@ -184,19 +192,29 @@ main = do initialise (Logger logger) ci httpMgr = do currentTime <- getCurrentTime - res <- runAsAdmin ci httpMgr $ initCatalogSafe currentTime - either printErrJExit (logger . mkGenericStrLog "db_init") res - migrate (Logger logger) ci httpMgr = do - currentTime <- getCurrentTime - res <- runAsAdmin ci httpMgr $ migrateCatalog currentTime - either printErrJExit (logger . mkGenericStrLog "db_migrate") res + -- initialise the catalog + initRes <- runAsAdmin ci httpMgr $ initCatalogSafe currentTime + either printErrJExit (logger . mkGenericStrLog "db_init") initRes + + -- migrate catalog if necessary + migRes <- runAsAdmin ci httpMgr $ migrateCatalog currentTime + either printErrJExit (logger . mkGenericStrLog "db_migrate") migRes + + -- generate and retrieve uuids + getUniqIds ci prepareEvents (Logger logger) ci = do logger $ mkGenericStrLog "event_triggers" "preparing data" res <- runTx ci unlockAllEvents either printErrJExit return res + getUniqIds ci = do + eDbId <- runTx ci getDbId + dbId <- either printErrJExit return eDbId + fp <- liftIO generateFingerprint + return (dbId, fp) + getFromEnv :: (Read a) => a -> String -> IO a getFromEnv defaults env = do mEnv <- lookupEnv env @@ -208,3 +226,10 @@ main = do cleanSuccess = putStrLn "successfully cleaned graphql-engine related data" + + +telemetryNotice :: String +telemetryNotice = + "Help us improve Hasura! The graphql-engine server collects anonymized " + <> "usage stats which allows us to keep improving Hasura at warp speed. " + <> "To read more or opt-out, visit https://docs.hasura.io/1.0/graphql/manual/guides/telemetry.html" diff --git a/server/src-exec/Ops.hs b/server/src-exec/Ops.hs index cfbb3267b3f..f66fc95ed3e 100644 --- a/server/src-exec/Ops.hs +++ b/server/src-exec/Ops.hs @@ -24,7 +24,7 @@ import qualified Database.PG.Query as Q import qualified Database.PG.Query.Connection as Q curCatalogVer :: T.Text -curCatalogVer = "8" +curCatalogVer = "9" initCatalogSafe :: (QErrM m, UserInfoM m, CacheRWM m, MonadTx m, MonadIO m, HasHttpManager m) @@ -102,7 +102,8 @@ initCatalogStrict createSchema initTime = do addVersion modTime = liftTx $ Q.catchE defaultTxErrorHandler $ Q.unitQ [Q.sql| - INSERT INTO "hdb_catalog"."hdb_version" VALUES ($1, $2) + INSERT INTO "hdb_catalog"."hdb_version" + (version, upgraded_on) VALUES ($1, $2) |] (curCatalogVer, modTime) False isExtAvailable :: T.Text -> Q.Tx Bool @@ -178,6 +179,17 @@ setAsSystemDefinedFor8 = AND table_name = 'hdb_function_agg'; |] +setAsSystemDefinedFor9 :: (MonadTx m) => m () +setAsSystemDefinedFor9 = + liftTx $ Q.catchE defaultTxErrorHandler $ + Q.multiQ [Q.sql| + UPDATE hdb_catalog.hdb_table + SET is_system_defined = 'true' + WHERE table_schema = 'hdb_catalog' + AND table_name = 'hdb_version'; + |] + + cleanCatalog :: (MonadTx m) => m () cleanCatalog = liftTx $ Q.catchE defaultTxErrorHandler $ do -- This is where the generated views and triggers are stored @@ -293,6 +305,21 @@ from7To8 = do migrateMetadataFrom7 = $(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_7_to_8.yaml" :: Q (TExp RQLQuery))) +-- alter hdb_version table and track it (telemetry changes) +from8To9 + :: (MonadTx m, HasHttpManager m, CacheRWM m, UserInfoM m, MonadIO m) + => m () +from8To9 = do + Q.Discard () <- liftTx $ Q.multiQE defaultTxErrorHandler + $(Q.sqlFromFile "src-rsr/migrate_from_8_to_9.sql") + void $ runQueryM migrateMetadataFrom8 + -- set as system defined + setAsSystemDefinedFor9 + where + migrateMetadataFrom8 = + $(unTypeQ (Y.decodeFile "src-rsr/migrate_metadata_from_8_to_9.yaml" :: Q (TExp RQLQuery))) + + migrateCatalog :: (MonadTx m, CacheRWM m, MonadIO m, UserInfoM m, HasHttpManager m) => UTCTime -> m String @@ -308,12 +335,17 @@ migrateCatalog migrationTime = do | preVer == "5" -> from5ToCurrent | preVer == "6" -> from6ToCurrent | preVer == "7" -> from7ToCurrent + | preVer == "8" -> from8ToCurrent | otherwise -> throw400 NotSupported $ "unsupported version : " <> preVer where + from8ToCurrent = do + from8To9 + postMigrate + from7ToCurrent = do from7To8 - postMigrate + from8ToCurrent from6ToCurrent = do from6To7 diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index 7e3b060fe98..af8374fe52a 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -55,9 +55,12 @@ import Hasura.SQL.Types consoleTmplt :: M.Template consoleTmplt = $(M.embedSingleTemplate "src-rsr/console.html") +boolToText :: Bool -> T.Text +boolToText = bool "false" "true" + isAccessKeySet :: AuthMode -> T.Text -isAccessKeySet AMNoAuth = "false" -isAccessKeySet _ = "true" +isAccessKeySet AMNoAuth = boolToText False +isAccessKeySet _ = boolToText True #ifdef LocalConsole consoleAssetsLoc :: Text @@ -68,14 +71,15 @@ consoleAssetsLoc = "https://storage.googleapis.com/hasura-graphql-engine/console/" <> consoleVersion #endif -mkConsoleHTML :: T.Text -> AuthMode -> Either String T.Text -mkConsoleHTML path authMode = +mkConsoleHTML :: T.Text -> AuthMode -> Bool -> Either String T.Text +mkConsoleHTML path authMode enableTelemetry = bool (Left errMsg) (Right res) $ null errs where (errs, res) = M.checkedSubstitute consoleTmplt $ object [ "consoleAssetsLoc" .= consoleAssetsLoc , "isAccessKeySet" .= isAccessKeySet authMode , "consolePath" .= consolePath + , "enableTelemetry" .= boolToText enableTelemetry ] consolePath = case path of "" -> "/console" @@ -284,8 +288,9 @@ mkWaiApp -> AuthMode -> CorsConfig -> Bool + -> Bool -> IO (Wai.Application, IORef SchemaCache) -mkWaiApp isoLevel loggerCtx pool httpManager mode corsCfg enableConsole = do +mkWaiApp isoLevel loggerCtx pool httpManager mode corsCfg enableConsole enableTelemetry = do cacheRef <- do pgResp <- runExceptT $ peelRun emptySchemaCache adminUserInfo httpManager pool Q.Serializable $ do @@ -300,7 +305,7 @@ mkWaiApp isoLevel loggerCtx pool httpManager mode corsCfg enableConsole = do cacheLock mode httpManager spockApp <- spockAsApp $ spockT id $ - httpApp corsCfg serverCtx enableConsole + httpApp corsCfg serverCtx enableConsole enableTelemetry let runTx tx = runExceptT $ runLazyTx pool isoLevel tx @@ -308,8 +313,8 @@ mkWaiApp isoLevel loggerCtx pool httpManager mode corsCfg enableConsole = do let wsServerApp = WS.createWSServerApp mode wsServerEnv return (WS.websocketsOr WS.defaultConnectionOptions wsServerApp spockApp, cacheRef) -httpApp :: CorsConfig -> ServerCtx -> Bool -> SpockT IO () -httpApp corsCfg serverCtx enableConsole = do +httpApp :: CorsConfig -> ServerCtx -> Bool -> Bool -> SpockT IO () +httpApp corsCfg serverCtx enableConsole enableTelemetry = do -- cors middleware unless (ccDisabled corsCfg) $ middleware $ corsMiddleware (mkDefaultCorsPolicy $ ccDomain corsCfg) @@ -381,7 +386,7 @@ httpApp corsCfg serverCtx enableConsole = do get root $ redirect "console" get ("console" wildcard) $ \path -> either (raiseGenericApiError . err500 Unexpected . T.pack) html $ - mkConsoleHTML path $ scAuthMode serverCtx + mkConsoleHTML path (scAuthMode serverCtx) enableTelemetry #ifdef LocalConsole get "static/main.js" $ do diff --git a/server/src-lib/Hasura/Server/Init.hs b/server/src-lib/Hasura/Server/Init.hs index d9a911f5997..ef14e2ed505 100644 --- a/server/src-lib/Hasura/Server/Init.hs +++ b/server/src-lib/Hasura/Server/Init.hs @@ -6,10 +6,9 @@ import Options.Applicative import System.Exit (exitFailure) import qualified Data.Aeson as J +import qualified Data.String as DataString import qualified Data.Text as T import qualified Hasura.Logging as L -import qualified Text.PrettyPrint.ANSI.Leijen as PP -import qualified Data.String as DataString import Hasura.Prelude import Hasura.RQL.DDL.Utils import Hasura.RQL.Types (RoleName (..)) @@ -17,6 +16,7 @@ import Hasura.Server.Auth import Hasura.Server.Logging import Hasura.Server.Utils import Network.Wai.Handler.Warp +import qualified Text.PrettyPrint.ANSI.Leijen as PP initErrExit :: (Show e) => e -> IO a @@ -38,16 +38,17 @@ type RawAuthHook = AuthHookG (Maybe T.Text) (Maybe AuthHookType) data RawServeOptions = RawServeOptions - { rsoPort :: !(Maybe Int) - , rsoHost :: !(Maybe HostPreference) - , rsoConnParams :: !RawConnParams - , rsoTxIso :: !(Maybe Q.TxIsolation) - , rsoAccessKey :: !(Maybe AccessKey) - , rsoAuthHook :: !RawAuthHook - , rsoJwtSecret :: !(Maybe Text) - , rsoUnAuthRole :: !(Maybe RoleName) - , rsoCorsConfig :: !RawCorsConfig - , rsoEnableConsole :: !Bool + { rsoPort :: !(Maybe Int) + , rsoHost :: !(Maybe HostPreference) + , rsoConnParams :: !RawConnParams + , rsoTxIso :: !(Maybe Q.TxIsolation) + , rsoAccessKey :: !(Maybe AccessKey) + , rsoAuthHook :: !RawAuthHook + , rsoJwtSecret :: !(Maybe Text) + , rsoUnAuthRole :: !(Maybe RoleName) + , rsoCorsConfig :: !RawCorsConfig + , rsoEnableConsole :: !Bool + , rsoEnableTelemetry :: !(Maybe Bool) } deriving (Show, Eq) data CorsConfigG a @@ -61,16 +62,17 @@ type CorsConfig = CorsConfigG T.Text data ServeOptions = ServeOptions - { soPort :: !Int - , soHost :: !HostPreference - , soConnParams :: !Q.ConnParams - , soTxIso :: !Q.TxIsolation - , soAccessKey :: !(Maybe AccessKey) - , soAuthHook :: !(Maybe AuthHook) - , soJwtSecret :: !(Maybe Text) - , soUnAuthRole :: !(Maybe RoleName) - , soCorsConfig :: !CorsConfig - , soEnableConsole :: !Bool + { soPort :: !Int + , soHost :: !HostPreference + , soConnParams :: !Q.ConnParams + , soTxIso :: !Q.TxIsolation + , soAccessKey :: !(Maybe AccessKey) + , soAuthHook :: !(Maybe AuthHook) + , soJwtSecret :: !(Maybe Text) + , soUnAuthRole :: !(Maybe RoleName) + , soCorsConfig :: !CorsConfig + , soEnableConsole :: !Bool + , soEnableTelemetry :: !Bool } deriving (Show, Eq) data RawConnInfo = @@ -218,13 +220,16 @@ mkServeOptions rso = do withEnv (rsoTxIso rso) (fst txIsoEnv) accKey <- withEnv (rsoAccessKey rso) $ fst accessKeyEnv authHook <- mkAuthHook $ rsoAuthHook rso - jwtSecr <- withEnv (rsoJwtSecret rso) $ fst jwtSecretEnv + jwtSecret <- withEnv (rsoJwtSecret rso) $ fst jwtSecretEnv unAuthRole <- withEnv (rsoUnAuthRole rso) $ fst unAuthRoleEnv corsCfg <- mkCorsConfig $ rsoCorsConfig rso enableConsole <- withEnvBool (rsoEnableConsole rso) $ fst enableConsoleEnv - return $ ServeOptions port host connParams txIso accKey authHook - jwtSecr unAuthRole corsCfg enableConsole + enableTelemetry <- fromMaybe True <$> + withEnv (rsoEnableTelemetry rso) (fst enableTelemetryEnv) + + return $ ServeOptions port host connParams txIso accKey authHook jwtSecret + unAuthRole corsCfg enableConsole enableTelemetry where mkConnParams (RawConnParams s c i p) = do stripes <- fromMaybe 1 <$> withEnv s (fst pgStripesEnv) @@ -260,7 +265,7 @@ mkEnvVarDoc envVars = PP.indent 2 (PP.vsep $ map mkEnvVarLine envVars) where mkEnvVarLine (var, desc) = - (PP.fillBreak 30 (PP.text var) PP.<+> prettifyDesc desc) <> PP.hardline + (PP.fillBreak 40 (PP.text var) PP.<+> prettifyDesc desc) <> PP.hardline prettifyDesc = PP.align . PP.fillSep . map PP.text . words mainCmdFooter :: PP.Doc @@ -316,6 +321,9 @@ serveCmdFooter = , "graphql-engine --database-url serve --access-key " <> " --auth-hook https://mywebhook.com/post --auth-hook-mode POST" ] + , [ "# Start GraphQL Engine with telemetry enabled/disabled" + , "graphql-engine --database-url serve --enable-telemetry true|false" + ] ] envVarDoc = mkEnvVarDoc $ envVars <> eventEnvs @@ -323,6 +331,7 @@ serveCmdFooter = [ servePortEnv, serveHostEnv, pgStripesEnv, pgConnsEnv, pgTimeoutEnv , txIsoEnv, accessKeyEnv, authHookEnv , authHookModeEnv , jwtSecretEnv , unAuthRoleEnv, corsDomainEnv , enableConsoleEnv + , enableTelemetryEnv ] eventEnvs = @@ -418,6 +427,13 @@ enableConsoleEnv = , "Enable API Console" ) +enableTelemetryEnv :: (String, String) +enableTelemetryEnv = + ( "HASURA_GRAPHQL_ENABLE_TELEMETRY" + -- TODO: better description + , "Enable anonymous telemetry (default: true)" + ) + parseRawConnInfo :: Parser RawConnInfo parseRawConnInfo = RawConnInfo <$> host <*> port <*> user <*> password @@ -611,6 +627,13 @@ parseEnableConsole = help (snd enableConsoleEnv) ) +parseEnableTelemetry :: Parser (Maybe Bool) +parseEnableTelemetry = optional $ + option (eitherReader parseStrAsBool) + ( long "enable-telemetry" <> + help (snd enableTelemetryEnv) + ) + -- Init logging related connInfoToLog :: Q.ConnInfo -> StartupLog connInfoToLog (Q.ConnInfo host port user _ db _) = @@ -634,6 +657,7 @@ serveOptsToLog so = , "cors_domain" J..= (ccDomain . soCorsConfig) so , "cors_disabled" J..= (ccDisabled . soCorsConfig) so , "enable_console" J..= soEnableConsole so + , "enable_telemetry" J..= soEnableTelemetry so , "use_prepared_statements" J..= (Q.cpAllowPrepare . soConnParams) so ] diff --git a/server/src-lib/Hasura/Server/Telemetry.hs b/server/src-lib/Hasura/Server/Telemetry.hs new file mode 100644 index 00000000000..f55c6511e90 --- /dev/null +++ b/server/src-lib/Hasura/Server/Telemetry.hs @@ -0,0 +1,223 @@ +{-| + Send anonymized metrics to the telemetry server regarding usage of various + features of Hasura. +-} + +module Hasura.Server.Telemetry + ( runTelemetry + , getDbId + , generateFingerprint + , mkTelemetryLog + ) + where + +import Control.Exception (try) +import Control.Lens +import Data.IORef +import Data.List + +import Hasura.HTTP +import Hasura.Logging +import Hasura.Prelude +import Hasura.RQL.Types +import Hasura.Server.Version + +import qualified Control.Concurrent as C +import qualified Data.Aeson as A +import qualified Data.Aeson.Casing as A +import qualified Data.Aeson.TH as A +import qualified Data.ByteString.Lazy as BL +import qualified Data.HashMap.Strict as Map +import qualified Data.String.Conversions as CS +import qualified Data.Text as T +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as UUID +import qualified Database.PG.Query as Q +import qualified Network.HTTP.Client as HTTP +import qualified Network.HTTP.Types as HTTP +import qualified Network.Wreq as Wreq + + +data RelationshipMetric + = RelationshipMetric + { _rmManual :: !Int + , _rmAuto :: !Int + } deriving (Show, Eq) +$(A.deriveJSON (A.aesonDrop 3 A.snakeCase) ''RelationshipMetric) + +data PermissionMetric + = PermissionMetric + { _pmSelect :: !Int + , _pmInsert :: !Int + , _pmUpdate :: !Int + , _pmDelete :: !Int + , _pmRoles :: !Int + } deriving (Show, Eq) +$(A.deriveJSON (A.aesonDrop 3 A.snakeCase) ''PermissionMetric) + +data Metrics + = Metrics + { _mtTables :: !Int + , _mtViews :: !Int + , _mtRelationships :: !RelationshipMetric + , _mtPermissions :: !PermissionMetric + , _mtEventTriggers :: !Int + , _mtRemoteSchemas :: !Int + } deriving (Show, Eq) +$(A.deriveJSON (A.aesonDrop 3 A.snakeCase) ''Metrics) + +data HasuraTelemetry + = HasuraTelemetry + { _htDbUid :: !Text + , _htInstanceUid :: !Text + , _htVersion :: !Text + , _htMetrics :: !Metrics + } deriving (Show, Eq) +$(A.deriveJSON (A.aesonDrop 3 A.snakeCase) ''HasuraTelemetry) + +data TelemetryPayload + = TelemetryPayload + { _tpTopic :: !Text + , _tpData :: !HasuraTelemetry + } deriving (Show, Eq) +$(A.deriveJSON (A.aesonDrop 3 A.snakeCase) ''TelemetryPayload) + +telemetryUrl :: Text +telemetryUrl = "https://telemetry.hasura.io/v1/http" + +mkPayload :: Text -> Text -> Text -> Metrics -> TelemetryPayload +mkPayload dbId instanceId version metrics = + TelemetryPayload topic $ HasuraTelemetry dbId instanceId version metrics + where topic = bool "server" "server_test" isDevVersion + +runTelemetry + :: Logger + -> HTTP.Manager + -> IORef SchemaCache + -> (Text, Text) + -> IO () +runTelemetry (Logger logger) manager cacheRef (dbId, instanceId) = do + let options = wreqOptions manager [] + forever $ do + schemaCache <- readIORef cacheRef + let metrics = computeMetrics schemaCache + payload = A.encode $ mkPayload dbId instanceId currentVersion metrics + logger $ debugLBS $ "metrics_info: " <> payload + resp <- try $ Wreq.postWith options (T.unpack telemetryUrl) payload + either logHttpEx handleHttpResp resp + C.threadDelay aDay + + where + logHttpEx :: HTTP.HttpException -> IO () + logHttpEx ex = do + let httpErr = Just $ mkHttpError telemetryUrl Nothing (Just $ HttpException ex) + logger $ mkTelemetryLog "http_exception" "http exception occurred" httpErr + + handleHttpResp resp = do + let statusCode = resp ^. Wreq.responseStatus . Wreq.statusCode + logger $ debugLBS $ "http_success: " <> resp ^. Wreq.responseBody + when (statusCode /= 200) $ do + let httpErr = Just $ mkHttpError telemetryUrl (Just resp) Nothing + logger $ mkTelemetryLog "http_error" "failed to post telemetry" httpErr + + aDay = 86400 * 1000 * 1000 + +computeMetrics :: SchemaCache -> Metrics +computeMetrics sc = + let nTables = Map.size $ Map.filter (isNothing . tiViewInfo) usrTbls + nViews = Map.size $ Map.filter (isJust . tiViewInfo) usrTbls + allRels = join $ Map.elems $ Map.map relsOfTbl usrTbls + (manualRels, autoRels) = partition riIsManual allRels + relMetrics = RelationshipMetric (length manualRels) (length autoRels) + rolePerms = join $ Map.elems $ Map.map permsOfTbl usrTbls + nRoles = length $ nub $ fst <$> rolePerms + allPerms = snd <$> rolePerms + insPerms = calcPerms _permIns allPerms + selPerms = calcPerms _permSel allPerms + updPerms = calcPerms _permUpd allPerms + delPerms = calcPerms _permDel allPerms + permMetrics = + PermissionMetric selPerms insPerms updPerms delPerms nRoles + evtTriggers = Map.size $ Map.filter (not . Map.null) + $ Map.map tiEventTriggerInfoMap usrTbls + rmSchemas = Map.size $ scRemoteResolvers sc + + in Metrics nTables nViews relMetrics permMetrics evtTriggers rmSchemas + + where + usrTbls = Map.filter (not . tiSystemDefined) $ scTables sc + + calcPerms :: (RolePermInfo -> Maybe a) -> [RolePermInfo] -> Int + calcPerms fn perms = length $ catMaybes $ map fn perms + + relsOfTbl :: TableInfo -> [RelInfo] + relsOfTbl = rights . Map.elems . Map.map fieldInfoToEither . tiFieldInfoMap + + permsOfTbl :: TableInfo -> [(RoleName, RolePermInfo)] + permsOfTbl = Map.toList . tiRolePermInfoMap + + +generateFingerprint :: IO Text +generateFingerprint = UUID.toText <$> UUID.nextRandom + +getDbId :: Q.TxE QErr Text +getDbId = + (runIdentity . Q.getRow) <$> + Q.withQE defaultTxErrorHandler + [Q.sql| + SELECT (hasura_uuid :: text) FROM hdb_catalog.hdb_version + |] () False + + +-- | Logging related + +data TelemetryLog + = TelemetryLog + { _tlLogLevel :: !LogLevel + , _tlType :: !Text + , _tlMessage :: !Text + , _tlHttpError :: !(Maybe TelemetryHttpError) + } deriving (Show) + +data TelemetryHttpError + = TelemetryHttpError + { tlheStatus :: !(Maybe HTTP.Status) + , tlheUrl :: !T.Text + , tlheHttpException :: !(Maybe HttpException) + , tlheResponse :: !(Maybe T.Text) + } deriving (Show) + +instance A.ToJSON TelemetryLog where + toJSON tl = + A.object [ "type" A..= _tlType tl + , "message" A..= _tlMessage tl + , "http_error" A..= (A.toJSON <$> _tlHttpError tl) + ] + +instance A.ToJSON TelemetryHttpError where + toJSON tlhe = + A.object [ "status_code" A..= (HTTP.statusCode <$> tlheStatus tlhe) + , "url" A..= tlheUrl tlhe + , "response" A..= tlheResponse tlhe + , "http_exception" A..= (A.toJSON <$> tlheHttpException tlhe) + ] + + +instance ToEngineLog TelemetryLog where + toEngineLog tl = (_tlLogLevel tl, "telemetry-log", A.toJSON tl) + +mkHttpError + :: Text + -> Maybe (Wreq.Response BL.ByteString) + -> Maybe HttpException + -> TelemetryHttpError +mkHttpError url mResp httpEx = + case mResp of + Nothing -> TelemetryHttpError Nothing url httpEx Nothing + Just resp -> + let status = resp ^. Wreq.responseStatus + body = CS.cs $ resp ^. Wreq.responseBody + in TelemetryHttpError (Just status) url httpEx (Just body) + +mkTelemetryLog :: Text -> Text -> Maybe TelemetryHttpError -> TelemetryLog +mkTelemetryLog = TelemetryLog LevelInfo diff --git a/server/src-lib/Hasura/Server/Version.hs b/server/src-lib/Hasura/Server/Version.hs index 43b1a5f8370..f6c791d008a 100644 --- a/server/src-lib/Hasura/Server/Version.hs +++ b/server/src-lib/Hasura/Server/Version.hs @@ -1,6 +1,7 @@ module Hasura.Server.Version ( currentVersion , consoleVersion + , isDevVersion ) where @@ -28,3 +29,7 @@ mkVersion ver = T.pack $ "v" ++ show major ++ "." ++ show minor currentVersion :: T.Text currentVersion = version + +isDevVersion :: Bool +isDevVersion = either (const True) (const False) $ + V.fromText $ T.dropWhile (== 'v') version diff --git a/server/src-rsr/console.html b/server/src-rsr/console.html index f233858d082..f5781f1a337 100644 --- a/server/src-rsr/console.html +++ b/server/src-rsr/console.html @@ -6,7 +6,8 @@ consoleMode: "server", urlPrefix: "/console", consolePath: "{{consolePath}}", - isAccessKeySet: {{isAccessKeySet}} + isAccessKeySet: {{isAccessKeySet}}, + enableTelemetry: {{enableTelemetry}} }; diff --git a/server/src-rsr/hdb_metadata.yaml b/server/src-rsr/hdb_metadata.yaml index f15fd98fe06..5c02ad91609 100644 --- a/server/src-rsr/hdb_metadata.yaml +++ b/server/src-rsr/hdb_metadata.yaml @@ -260,6 +260,11 @@ args: schema: hdb_catalog name: remote_schemas +- type: track_table + args: + schema: hdb_catalog + name: hdb_version + - type: create_object_relationship args: name: return_table_info diff --git a/server/src-rsr/initialise.sql b/server/src-rsr/initialise.sql index 9a2f90bc354..9bcfe134124 100644 --- a/server/src-rsr/initialise.sql +++ b/server/src-rsr/initialise.sql @@ -1,6 +1,9 @@ CREATE TABLE hdb_catalog.hdb_version ( + hasura_uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(), version TEXT NOT NULL, - upgraded_on TIMESTAMPTZ NOT NULL + upgraded_on TIMESTAMPTZ NOT NULL, + cli_state JSONB NOT NULL DEFAULT '{}'::jsonb, + console_state JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE UNIQUE INDEX hdb_version_one_row diff --git a/server/src-rsr/migrate_from_8_to_9.sql b/server/src-rsr/migrate_from_8_to_9.sql new file mode 100644 index 00000000000..f29a7d3f3bd --- /dev/null +++ b/server/src-rsr/migrate_from_8_to_9.sql @@ -0,0 +1,5 @@ +ALTER TABLE hdb_catalog.hdb_version + ADD COLUMN hasura_uuid UUID DEFAULT gen_random_uuid(), + ADD COLUMN cli_state JSONB NOT NULL DEFAULT '{}'::jsonb, + ADD COLUMN console_state JSONB NOT NULL DEFAULT '{}'::jsonb, + ADD CONSTRAINT hasura_uuid_pkey PRIMARY KEY (hasura_uuid); diff --git a/server/src-rsr/migrate_metadata_from_8_to_9.yaml b/server/src-rsr/migrate_metadata_from_8_to_9.yaml new file mode 100644 index 00000000000..1898baea471 --- /dev/null +++ b/server/src-rsr/migrate_metadata_from_8_to_9.yaml @@ -0,0 +1,4 @@ +type: track_table +args: + schema: hdb_catalog + name: hdb_version diff --git a/server/tests-py/graphql_server.py b/server/tests-py/graphql_server.py index 6ee1c93cf04..a37126a1f50 100644 --- a/server/tests-py/graphql_server.py +++ b/server/tests-py/graphql_server.py @@ -241,5 +241,5 @@ def stop_server(server): server.server_close() if __name__ == '__main__': - s = create_server() + s = create_server(host='0.0.0.0') s.serve_forever()