Ctrl K
ComunidadGuías…

pkg/telemetry/telemetry.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package telemetry sends usage events to the ShellHub telemetry collector, which speaks
// PostHog's capture v1 protocol. It is the only package that imports posthog-go.
package telemetry

import (
	"maps"
	"time"

	"github.com/posthog/posthog-go"
	log "github.com/sirupsen/logrus"
)

// DefaultEndpoint is the collector an instance reports to. Released binaries carry it, so it
// never changes.
const DefaultEndpoint = "https://telemetry.shellhub.io"

// SchemaVersion is sent as schema_version on every event. Raise it when a property changes
// meaning or is removed, so the collector can tell old payloads from new ones.
const SchemaVersion = 1

// CommunityWriteKey and EnterpriseWriteKey are the public write keys the Community and
// Enterprise binaries send. They are not secrets: they filter stray traffic and name the edition.
// Cloud's key is a secret its deployment provides.
const (
	CommunityWriteKey  = "shellhub_community_f7319c57cf0f033d6c7128f3eafc4b4b"
	EnterpriseWriteKey = "shellhub_enterprise_277b7aebd2fbd069ce337546bf36adf2"
)

const shutdownTimeout = 3 * time.Second

// Event is one telemetry event. Name is an object and a past-tense verb, written as a literal,
// and never starts with "$", which PostHog reserves.
type Event struct {
	Name       string
	DistinctID string
	Properties map[string]any
	Groups     map[string]string
}

// ClientProperties are the properties posthog-go adds to every event on its own, besides the
// uuid and timestamp. The public telemetry page lists them; $os_version and $os_distro are sent
// only when the host reports them.
var ClientProperties = []string{"$os", "$os_version", "$os_distro", "$go_version", "$is_server", "$geoip_disable"}

// Preview is the event as the collector receives it, without the uuid, the timestamp and the
// [ClientProperties] posthog-go adds when it sends it.
type Preview struct {
	Event      string         `json:"event"`
	DistinctID string         `json:"distinct_id"`
	Options    map[string]any `json:"options"`
	Properties map[string]any `json:"properties"`
}

// Preview returns the event as posthog-go sends it: schema_version added, $groups added when the
// event has groups, and $process_person_profile moved out of Properties into Options.
func (e Event) Preview() Preview {
	properties := e.properties()
	delete(properties, "$process_person_profile")

	return Preview{
		Event:      e.Name,
		DistinctID: e.DistinctID,
		Options:    map[string]any{"process_person_profile": false},
		Properties: properties,
	}
}

func (e Event) properties() map[string]any {
	properties := make(map[string]any, len(e.Properties)+3)
	maps.Copy(properties, e.Properties)

	properties["schema_version"] = SchemaVersion
	properties["$process_person_profile"] = false

	if len(e.Groups) > 0 {
		groups := make(map[string]any, len(e.Groups))
		for name, key := range e.Groups {
			groups[name] = key
		}

		properties["$groups"] = groups
	}

	return properties
}

// Client queues events and sends them to the collector in the background.
type Client struct {
	posthog  posthog.Client
	endpoint string
}

// New returns a client that sends to endpoint, authenticating with the edition's write key. It
// returns an error when posthog-go rejects the configuration.
func New(endpoint, writeKey string) (*Client, error) {
	client, err := posthog.NewWithConfig(writeKey, posthog.Config{
		Endpoint:        endpoint,
		CaptureMode:     posthog.CaptureModeAnalyticsV1,
		Compression:     posthog.CompressionGzip,
		ShutdownTimeout: shutdownTimeout,
		Callback:        callback{},
		Logger:          logger{},
	})
	if err != nil {
		return nil, err
	}

	return &Client{posthog: client, endpoint: endpoint}, nil
}

// Endpoint returns the collector the client sends to.
func (c *Client) Endpoint() string {
	return c.endpoint
}

// Send queues events for delivery. It returns the first error posthog-go reports, such as
// posthog.ErrQueueFull, after trying to queue every event. A queued event the collector drops or
// never receives is logged at debug level, not returned.
func (c *Client) Send(events ...Event) error {
	var first error

	for _, event := range events {
		err := c.posthog.Enqueue(posthog.Capture{
			DistinctId: event.DistinctID,
			Event:      event.Name,
			Properties: event.properties(),
		})
		if err != nil && first == nil {
			first = err
		}
	}

	return first
}

// Close flushes the queued events and stops the client, giving up after a few seconds so a
// collector that does not answer cannot hold up a shutdown. It returns the error posthog-go's
// Close reports when the queue cannot be flushed in time or the client is already closed.
func (c *Client) Close() error {
	return c.posthog.Close()
}

type callback struct{}

func (callback) Success(posthog.APIMessage) {}

func (callback) Failure(message posthog.APIMessage, err error) {
	entry := log.WithError(err)
	if capture, ok := message.(posthog.CaptureInApi); ok {
		entry = entry.WithField("event", capture.Event)
	}

	entry.Debug("the telemetry collector did not accept an event")
}

type logger struct{}

func (logger) Debugf(format string, args ...any) { log.Debugf("telemetry: "+format, args...) }
func (logger) Logf(format string, args ...any)   { log.Debugf("telemetry: "+format, args...) }
func (logger) Warnf(format string, args ...any)  { log.Debugf("telemetry: "+format, args...) }
func (logger) Errorf(format string, args ...any) { log.Debugf("telemetry: "+format, args...) }