2021-07-20 13:11:20 +02:00
|
|
|
package telemetry
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2022-03-14 20:38:06 +01:00
|
|
|
"crypto/sha256"
|
2021-07-20 13:11:20 +02:00
|
|
|
"encoding/json"
|
2022-03-09 08:31:38 +01:00
|
|
|
"errors"
|
2022-03-14 20:38:06 +01:00
|
|
|
"fmt"
|
2021-07-20 13:11:20 +02:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2022-03-09 08:31:38 +01:00
|
|
|
"path/filepath"
|
2021-07-20 13:11:20 +02:00
|
|
|
"runtime"
|
|
|
|
"time"
|
|
|
|
|
2022-03-14 20:38:06 +01:00
|
|
|
"github.com/go-git/go-git/v5"
|
2021-07-20 13:11:20 +02:00
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/mitchellh/go-homedir"
|
|
|
|
"github.com/rs/zerolog/log"
|
2022-03-14 20:38:06 +01:00
|
|
|
"go.dagger.io/dagger/pkg"
|
2021-07-20 13:11:20 +02:00
|
|
|
"go.dagger.io/dagger/version"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2021-12-14 03:26:41 +01:00
|
|
|
apiKey = "cb9777c166aefe4b77b31f961508191c" //nolint
|
2021-07-20 13:11:20 +02:00
|
|
|
telemetryURL = "https://t.dagger.io/v1"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Property struct {
|
|
|
|
Name string
|
|
|
|
Value interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TrackAsync(ctx context.Context, eventName string, properties ...*Property) chan struct{} {
|
|
|
|
doneCh := make(chan struct{}, 1)
|
|
|
|
go func() {
|
|
|
|
defer close(doneCh)
|
|
|
|
Track(ctx, eventName, properties...)
|
|
|
|
}()
|
|
|
|
return doneCh
|
|
|
|
}
|
|
|
|
|
|
|
|
func Track(ctx context.Context, eventName string, properties ...*Property) {
|
|
|
|
lg := log.Ctx(ctx).
|
|
|
|
With().
|
|
|
|
Str("event", eventName).
|
|
|
|
Logger()
|
|
|
|
|
2022-03-14 20:38:06 +01:00
|
|
|
if telemetryDisabled() {
|
2021-07-20 13:11:20 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-18 20:29:44 +01:00
|
|
|
repo := gitRepoURL(ctx, ".")
|
2021-07-20 13:11:20 +02:00
|
|
|
|
|
|
|
// Base properties
|
|
|
|
props := map[string]interface{}{
|
|
|
|
"dagger_version": version.Version,
|
|
|
|
"dagger_revision": version.Revision,
|
|
|
|
"os": runtime.GOOS,
|
|
|
|
"arch": runtime.GOARCH,
|
|
|
|
}
|
|
|
|
|
2022-03-14 20:38:06 +01:00
|
|
|
if repo != "" {
|
|
|
|
// Hash the repository URL for privacy
|
|
|
|
props["git_repository_hash"] = hash(repo)
|
|
|
|
}
|
|
|
|
|
|
|
|
if projectDir, found := pkg.GetCueModParent(); found {
|
|
|
|
// Hash the project path for privacy
|
|
|
|
props["project_path_hash"] = hash(projectDir)
|
|
|
|
}
|
|
|
|
|
2021-07-20 13:11:20 +02:00
|
|
|
// Merge extra properties
|
|
|
|
for _, p := range properties {
|
|
|
|
props[p.Name] = p.Value
|
|
|
|
}
|
2022-03-14 20:38:06 +01:00
|
|
|
|
|
|
|
deviceID, err := getDeviceID(repo)
|
|
|
|
if err != nil {
|
|
|
|
lg.Trace().Err(err).Msg("failed to get device id")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
lg = lg.With().Str("device_id", deviceID).Fields(props).Logger()
|
2021-07-20 13:11:20 +02:00
|
|
|
|
|
|
|
ev := &event{
|
|
|
|
DeviceID: deviceID,
|
|
|
|
EventType: eventName,
|
|
|
|
Time: time.Now().Unix(),
|
|
|
|
AppVersion: version.Version,
|
|
|
|
OSName: runtime.GOOS,
|
|
|
|
Platform: runtime.GOARCH,
|
|
|
|
IP: "$remote", // Use "$remote" to use the IP address on the upload request
|
|
|
|
EventProperties: props,
|
|
|
|
}
|
|
|
|
|
|
|
|
p := &payload{
|
|
|
|
APIKey: apiKey,
|
|
|
|
Events: []*event{ev},
|
|
|
|
}
|
|
|
|
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
if err := json.NewEncoder(b).Encode(p); err != nil {
|
|
|
|
lg.Trace().Err(err).Msg("failed to encode payload")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", telemetryURL, b)
|
|
|
|
if err != nil {
|
|
|
|
lg.Trace().Err(err).Msg("failed to prepare request")
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header = map[string][]string{
|
|
|
|
"Content-Type": {"application/json"},
|
|
|
|
"Accept": {"*/*"},
|
|
|
|
}
|
|
|
|
|
2022-03-10 20:08:55 +01:00
|
|
|
start := time.Now()
|
2021-07-20 13:11:20 +02:00
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
lg.Trace().Err(err).Msg("failed to send telemetry event")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
lg.Trace().Str("status", resp.Status).Msg("telemetry request failed")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-10 20:08:55 +01:00
|
|
|
lg.Trace().Dur("duration", time.Since(start)).Msg("telemetry event")
|
2021-07-20 13:11:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type payload struct {
|
|
|
|
APIKey string `json:"api_key,omitempty"`
|
|
|
|
Events []*event `json:"events"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type event struct {
|
|
|
|
UserID string `json:"user_id,omitempty"`
|
|
|
|
DeviceID string `json:"device_id,omitempty"`
|
|
|
|
EventType string `json:"event_type,omitempty"`
|
|
|
|
Time int64 `json:"time,omitempty"`
|
|
|
|
AppVersion string `json:"app_version,omitempty"`
|
|
|
|
Platform string `json:"platform,omitempty"`
|
|
|
|
OSName string `json:"os_name,omitempty"`
|
|
|
|
OSVersion string `json:"os_version,omitempty"`
|
|
|
|
IP string `json:"ip,omitempty"`
|
|
|
|
EventProperties map[string]interface{} `json:"event_properties,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func isCI() bool {
|
|
|
|
return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
|
|
|
|
os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
|
|
|
|
os.Getenv("RUN_ID") != "" // TaskCluster, dsari
|
|
|
|
}
|
|
|
|
|
|
|
|
func telemetryDisabled() bool {
|
|
|
|
return os.Getenv("DAGGER_TELEMETRY_DISABLE") != "" || // dagger specific env
|
|
|
|
os.Getenv("DO_NOT_TRACK") != "" // https://consoledonottrack.com/
|
|
|
|
}
|
|
|
|
|
2022-03-14 20:38:06 +01:00
|
|
|
func getDeviceID(repo string) (string, error) {
|
|
|
|
if isCI() {
|
|
|
|
if repo == "" {
|
|
|
|
return "", fmt.Errorf("unable to determine device ID")
|
|
|
|
}
|
|
|
|
return "ci-" + hash(repo), nil
|
|
|
|
}
|
2021-07-20 13:11:20 +02:00
|
|
|
idFile, err := homedir.Expand("~/.config/dagger/cli_id")
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
id, err := os.ReadFile(idFile)
|
|
|
|
if err != nil {
|
2022-03-09 08:31:38 +01:00
|
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(idFile), 0755); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2021-07-20 13:11:20 +02:00
|
|
|
id = []byte(uuid.New().String())
|
|
|
|
if err := os.WriteFile(idFile, id, 0600); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return string(id), nil
|
|
|
|
}
|
2022-03-14 20:38:06 +01:00
|
|
|
|
|
|
|
// hash returns the sha256 digest of the string
|
|
|
|
func hash(s string) string {
|
|
|
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
|
|
|
|
}
|
|
|
|
|
2022-03-18 20:29:44 +01:00
|
|
|
// gitRepoURL returns the git repository remote, if any.
|
|
|
|
func gitRepoURL(ctx context.Context, path string) string {
|
2022-03-14 20:38:06 +01:00
|
|
|
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{
|
|
|
|
DetectDotGit: true,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
origin, err := repo.Remote("origin")
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2022-03-18 20:29:44 +01:00
|
|
|
urls := origin.Config().URLs
|
|
|
|
if len(urls) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
endpoint, err := parseGitURL(urls[0])
|
|
|
|
if err != nil {
|
|
|
|
log.Ctx(ctx).Debug().Err(err).Str("url", urls[0]).Msg("failed to parse git URL")
|
|
|
|
return ""
|
2022-03-14 20:38:06 +01:00
|
|
|
}
|
|
|
|
|
2022-03-18 20:29:44 +01:00
|
|
|
return endpoint
|
2022-03-14 20:38:06 +01:00
|
|
|
}
|