add anonymous telemetry (#1401)

This commit is contained in:
Shahidh K Muhammed 2019-01-28 19:25:28 +05:30 committed by Vamshi Surabhi
parent 8e3b8f51c9
commit 11e7c3f9d6
48 changed files with 1606 additions and 145 deletions

141
cli/Gopkg.lock generated
View File

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

View File

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

View File

@ -10,7 +10,9 @@
dataApiVersion: {{.dataApiVersion}},
accessKey: {{.accessKey}},
urlPrefix: "/",
consoleMode: "cli"
consoleMode: "cli",
cliUUID: {{.cliUUID}},
enableTelemetry: {{.enableTelemetry}}
};
</script>
</head>

View File

@ -10,7 +10,9 @@
dataApiVersion: {{.dataApiVersion}},
accessKey: {{.accessKey}},
urlPrefix: "/",
consoleMode: "cli"
consoleMode: "cli",
cliUUID: {{.cliUUID}},
enableTelemetry: {{.enableTelemetry}}
};
</script>
</head>

View File

@ -10,7 +10,9 @@
dataApiVersion: {{.dataApiVersion}},
accessKey: {{.accessKey}},
urlPrefix: "/",
consoleMode: "cli"
consoleMode: "cli",
cliUUID: {{.cliUUID}},
enableTelemetry: {{.enableTelemetry}}
};
</script>
</head>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

117
cli/get.sh Executable file
View File

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

120
cli/telemetry/telemetry.go Normal file
View File

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

View File

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

58
cli/util/server.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<ErrorBoundary>
<div>
@ -153,6 +164,7 @@ const mapStateToProps = state => {
return {
...state.progressBar,
notifications: state.notifications,
telemetry: state.telemetry,
};
};

View File

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

View File

@ -1116,3 +1116,10 @@
border: 0;
}
}
.telemetryNotification {
background-color: red;
padding: 10px;
position: fixed;
bottom: 50px;
right: 0px;
}

View File

@ -9,6 +9,7 @@ const defaultState = {
loginError: false,
serverVersion: null,
latestServerVersion: null,
telemetryEnabled: true,
};
export default defaultState;

View File

@ -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}
};`,
}}
/>

View File

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

View File

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

View File

@ -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: (
<div>
Help us improve Hasura! The console collects anonymized usage stats
which allows us to keep improving Hasura at warp speed.
<a
href="https://docs.hasura.io/1.0/graphql/manual/guides/telemetry.html"
target="_blank"
rel="noopener noreferrer"
>
{' '}
Click here
</a>{' '}
to read more or to opt-out.
</div>
),
onRemove: () => dispatch(onRemove()),
})
);
};
};
export { showTelemetryNotification };

View File

@ -0,0 +1,6 @@
const defaultTelemetryState = {
console_opts: null,
hasura_uuid: null,
};
export default defaultTelemetryState;

View File

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

View File

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

View File

@ -50,6 +50,10 @@ Postgres Auditing
- :doc:`Auditing tables <auditing-tables>`
Telemetry
---------
- :doc:`Guide on telemetry and instructions to opt-out <telemetry>`
.. toctree::
:maxdepth: 1
@ -61,3 +65,4 @@ Postgres Auditing
Integration/migration tutorials <integrations/index>
Integrating with monitoring frameworks <monitoring/index>
Auditing tables <auditing-tables>
Telemetry <telemetry>

View File

@ -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 <telemetry_optout>`.
.. 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 <https://hasura.io/legal/hasura-privacy-policy>`_.

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <database-url> serve --access-key <secretaccesskey>"
<> " --auth-hook https://mywebhook.com/post --auth-hook-mode POST"
]
, [ "# Start GraphQL Engine with telemetry enabled/disabled"
, "graphql-engine --database-url <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
]

View File

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

View File

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

View File

@ -6,7 +6,8 @@
consoleMode: "server",
urlPrefix: "/console",
consolePath: "{{consolePath}}",
isAccessKeySet: {{isAccessKeySet}}
isAccessKeySet: {{isAccessKeySet}},
enableTelemetry: {{enableTelemetry}}
};
</script>
</head>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
type: track_table
args:
schema: hdb_catalog
name: hdb_version

View File

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