Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add opt-out analytics to CLI (implements #108) #187

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))"
# ldflags -X injects commit version into binary

RUN license-check -path ./ --verbose=false \
&& go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \
&& OPEN_FAAS_TELEMETRY=0 go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it easier to put this as an ENV just above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, could do.

&& VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \
&& GIT_COMMIT=$(git rev-list -1 HEAD) \
&& CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile.redist
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))"
# ldflags -X injects commit version into binary

RUN license-check -path ./ --verbose=false \
&& go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \
&& OPEN_FAAS_TELEMETRY=0 go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \
&& VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \
&& GIT_COMMIT=$(git rev-list -1 HEAD) \
&& echo "Building linux..." \
&& CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli \
&& echo "Building darwin..." \
&& CGO_ENABLED=0 GOOS=darwin go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli-darwin \
&& echo "Building windows..." \
&& CGO_ENABLED=0 GOOS=windows go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli.exe \
&& echo "Building linux (arm)..." \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli-armhf \
&& echo "Building linux (arm64)..." \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli-arm64

FROM alpine:latest
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ build_redist:
./build_redist.sh

test-unit:
go test $(shell go list ./... | grep -v /vendor/ | grep -v /template/ | grep -v build) -cover
OPEN_FAAS_TELEMETRY=0 go test $(shell go list ./... | grep -v /vendor/ | grep -v /template/ | grep -v build) -cover
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ $ uname -a | curl http://localhost:8080/function/nodejs-echo--data-binary @-

> For further instructions on the manual CLI flags (without using a YAML file) read [manual_cli.md](https://github.com/openfaas/faas-cli/blob/master/MANUAL_CLI.md)

## Analytics

The `faas-cli` now gathers anonymous aggregate analytics on its use, for details on what and how data is gathered, along with how to opt-out read [analytics.md](analytics.md)

### FaaS-CLI Developers / Contributors

Expand Down
54 changes: 54 additions & 0 deletions analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Analytics Overview

The OpenFaaS `faas-cli` utility has begun gathering anonymous aggregate analytics on its use and reporting these to Google Analytics. You will be notified the first time you run `faas-cli`.

## Why?

OpenFaaS and its CLI are provided free of charge and run entirely by community volunteers in their spare time. As a result, we do not have the resources to do detailed user studies of OpenFaaS users to decide how best to design future features and prioritise work. Anonymous aggregate user analytics allow us to prioritise fixes and features based on how, where and when people use Homebrew.

## Where?

The OpenFaaS project sends analytics events to Google Analytics via HTTPS.

## What?

The `faas-cli` currently shares the following information each time a command is invoked.

- ProtocolVersion `v` - The Google Analytics Protocol version, currently `1`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#v
- Tracking ID `tid` - The `faas-cli` application tracking ID, e.g. `UA-107707760-2`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#tid
- Hit Type `t` - The type of analytics hit, `faas-cli` uses the `event` type.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#t
- Client ID `cid` - A `faas-cli` analytics user ID, e.g. `c1481af7-8682-462f-a586-71d752dbe87b`. This is a UUID4 generated by the [`github.com/satori/go.uuid`](https://github.com/satori/go.uuid) package and stored in the local config file `~/.openfaas/analytics_uuid`. This does not allow us to track individual users but does enable us to accurately measure user counts vs. event counts.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid
- ApplicationName `an` - The application name, e.g. `faas-cli`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#an
- ApplicationVersion `av` - The `faas-cli` version, e.g. `0.4.18d`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#av
- Anonymize IP `aip` - The IP address will be anonymized when submitting the analytics event, e.g. `1`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#aip
- Language `cd1` - A Custom Dimension containing the function language for `new`, `build` and `deploy` commands (returns `unset` for all other subcommands).
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_
- Operating System `cd2` - A Custom Dimension containing the OS on which `faas-cli` is being run, this is determined from `runtime.GOOS`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_
- Architecture `cd3` - A Custom Dimension containing the Architecture on which `faas-cli` is being run, this is determined from `runtime.GOOS`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_
- Event Category `ec` - The `faas-cli` groups all successful CLI actions under the `cli-success` category, and failures (not yet implemented) under `cli-failure`.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec
- Event Action `ea` - Each command invoked by the user is results in an analytics event being generated with the same name as the `faas-cli` subcommand.
- https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea

## How?

The core implementation is in the `analytics` package, with calls to `analytics.Event()` each time a subcommand is invoked.

Analytics submission occurs in a background goroutine and will fail fast to avoid delaying any execution.

Due to the speed with which most OpenFaaS API calls return, a timeout of 300 milliseconds is used to give the analytics submission time to complete.

## Opting out?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you link me to what brew/ VSCode says?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


If you wish to opt out of submitting analytics events you may do so by setting the `OPEN_FAAS_TELEMETRY` environment variable.

export OPEN_FAAS_TELEMETRY=0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our NS should be OPENFAAS_

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see where it was changed? The code review doesn't say "see outdated".. did you push yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed pushing the README it seems.

77 changes: 77 additions & 0 deletions analytics/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Alex Ellis 2017. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package analytics

import (
"fmt"
"io/ioutil"
"os"
"path"

homedir "github.com/mitchellh/go-homedir"
"github.com/satori/go.uuid"
)

const analyticsUUIDFile = "analytics_uuid"

func configDir() (string, error) {
h, err := homedir.Dir()
if err != nil {
return "", fmt.Errorf("unable to detect homedir: %v", err)
}
fullpath, err := homedir.Expand(h)
if err != nil {
return "", fmt.Errorf("unable to expand homedir [%s]: %v", h, err)
}

return path.Clean(fullpath + "/.openfaas/"), nil
}

func configFile() string {
dir, _ := configDir()
return path.Clean(dir + "/" + analyticsUUIDFile)
}

func configDirExists() bool {
dir, _ := configDir()
if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
return true
}
return false
}

func setUUID() (string, error) {
if !configDirExists() {
dir, _ := configDir()
err := os.Mkdir(dir, 0700)
if err != nil {
return "", fmt.Errorf("Unable to create config dir: %v\n", err)
}
}
uuidFile := configFile()
uuidStr := uuid.NewV4().String()
d1 := []byte(uuidStr)
err := ioutil.WriteFile(uuidFile, d1, 0644)
if err != nil {
return "", fmt.Errorf("unable to write analytics ID file: %v", err)
}
fmt.Fprintln(os.Stderr, "# Creating analytics file in:", uuidFile)
fmt.Fprintln(os.Stderr, "# Please see https://github.com/openfaas/faas-cli/blob/master/analytics.md for more information.")
return uuidStr, nil
}

func getUUID() (string, error) {
dat, err := ioutil.ReadFile(configFile())
if err != nil {
return "", fmt.Errorf("Error reading file: %v", err)
}
uuidStr := string(dat)

_, err = uuid.FromString(uuidStr)
if err != nil {
return "", fmt.Errorf("Unable to get valid UUID from file: %v", err)
}

return uuidStr, nil
}
94 changes: 94 additions & 0 deletions analytics/google_analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Alex Ellis 2017. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package analytics

import (
"fmt"
"net/http"
"net/url"
"os"
"runtime"

"github.com/google/go-querystring/query"
"github.com/openfaas/faas-cli/version"
)

const gaHost = "www.google-analytics.com"
const trackingID = "UA-107707760-3"
const applicationName = "faas-cli3"

// disableEnvvar will prevent submission of analytics events if set
const disableEnvvar = "OPEN_FAAS_TELEMETRY"

// Event posts an analytics event to GA
func Event(action string, language string, ch chan int) {
if Disabled() {
return
}
u, err := NewSession(language)
if err != nil {
return
}
u.EventAction = action
go u.PostEvent(ch)
}

// NewSession provides a setup UserSession struct with sane defaults
func NewSession(language string) (*UserSession, error) {
if len(language) == 0 {
language = "unset"
}
userSession := &UserSession{
HTTPClient: http.DefaultClient,
ProtocolVersion: 1,
Type: "event",
TrackingID: trackingID,
ApplicationName: applicationName,
ApplicationVersion: version.BuildVersion(),
AnonymizeIP: 1,
Language: language,
OS: runtime.GOOS,
ARCH: runtime.GOARCH,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought this may mis-report I think it's set at build-time. One thing it won't do is to show armv6/7/8 differences. For that an OS library like pstate etc might help - although we'd need one that doesn't use CGO.

EventCategory: "cli-success",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get whether /.dockerenv exists? Tells you whether you're in a container or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/.dockerenv will tell us whether the environment is a Docker container.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding this at the moment, just need to setup the GA custom dimension and Google Data Suite dashboard to validate it's getting pushed through.

}

uuid, err := getUUID()
if err != nil {
uuid, err = setUUID()
if err != nil {
return nil, fmt.Errorf("Unable to get or set Analytics Client ID")
}
}
userSession.ClientID = uuid

return userSession, nil
}

// PostEvent submits the generated event to Google Analytics, it is a
// fire and forget process and ignores the response
func (u UserSession) PostEvent(ch chan int) {
v, _ := query.Values(u)
req := &http.Request{
Method: http.MethodPost,
Host: gaHost,
URL: &url.URL{
Host: gaHost,
Scheme: "https",
Path: "/collect",
RawQuery: v.Encode(),
},
}
u.HTTPClient.Do(req)
ch <- 1
}

// Disabled returns true if the opt-out envvar has been set
// and false if it has not been set
func Disabled() bool {
val, ok := os.LookupEnv(disableEnvvar)
if ok && val == "0" {
return true
}
return false
}
24 changes: 24 additions & 0 deletions analytics/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Alex Ellis 2017. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package analytics

import "net/http"

// UserSession contains common Google Analytics event values and
// the client used to submit the event.
type UserSession struct {
HTTPClient *http.Client `url:"-"`
ProtocolVersion int `url:"v"`
Type string `url:"t"`
ClientID string `url:"cid"`
TrackingID string `url:"tid"`
ApplicationName string `url:"an"`
ApplicationVersion string `url:"av"`
AnonymizeIP int `url:"aip"`
Language string `url:"cd1"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cd1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom Dimension 1, its a Google Analytics term, the backend GA setup doc I shared should explain.

OS string `url:"cd2"`
ARCH string `url:"cd3"`
EventCategory string `url:"ec,omitempty"`
EventAction string `url:"ea,omitempty"`
}
5 changes: 5 additions & 0 deletions commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"sync"

"github.com/openfaas/faas-cli/analytics"
"github.com/openfaas/faas-cli/builder"
"github.com/openfaas/faas-cli/stack"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -97,6 +98,8 @@ func runBuild(cmd *cobra.Command, args []string) error {
if len(functionName) == 0 {
return fmt.Errorf("please provide the deployed --name of your function")
}
analytics.Event("build", language, analyticsCh)

builder.BuildImage(image, handler, functionName, language, nocache, squash, shrinkwrap)
}

Expand All @@ -118,6 +121,8 @@ func build(services *stack.Services, queueDepth int, shrinkwrap bool) {
fmt.Println("Please provide a valid --lang or 'Dockerfile' for your function.")

} else {
analytics.Event("build", function.Language, analyticsCh)

builder.BuildImage(function.Image, function.Handler, function.Name, function.Language, nocache, squash, shrinkwrap)
}
}
Expand Down
5 changes: 5 additions & 0 deletions commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"gopkg.in/yaml.v2"

"github.com/openfaas/faas-cli/analytics"
"github.com/openfaas/faas-cli/proxy"
"github.com/openfaas/faas-cli/stack"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -171,6 +172,8 @@ func runDeploy(cmd *cobra.Command, args []string) error {
}
}

analytics.Event("deploy", function.Language, analyticsCh)

functionResourceRequest1 := proxy.FunctionResourceRequest{
Limits: function.Limits,
Requests: function.Requests,
Expand All @@ -195,6 +198,8 @@ func runDeploy(cmd *cobra.Command, args []string) error {
if labelErr != nil {
return fmt.Errorf("error parsing labels: %v", labelErr)
}
analytics.Event("deploy", language, analyticsCh)

functionResourceRequest1 := proxy.FunctionResourceRequest{}
proxy.DeployFunction(fprocess, gateway, functionName, image, language, replace, envvars, network, constraints, update, secrets, labelMap, functionResourceRequest1)
}
Expand Down
Loading