feat(api): posthog telemetry (#374)

* feat: add posthog dep

* feat: posthog analytics

* feat: user events

* fix: nil tenant

* feat: tenant ident

* chore: linting

* Update pkg/analytics/posthog/posthog.go

Co-authored-by: abelanger5 <belanger@sas.upenn.edu>

* fix: typo

---------

Co-authored-by: abelanger5 <belanger@sas.upenn.edu>
This commit is contained in:
Gabe Ruttner
2024-04-12 06:16:14 -07:00
committed by GitHub
parent f16a9cd0dc
commit ca68eee45c
13 changed files with 190 additions and 2 deletions

View File

@@ -58,6 +58,18 @@ func (t *TenantService) TenantCreate(ctx echo.Context, request gen.TenantCreateR
return nil, err
}
t.config.Analytics.Tenant(tenant.ID, map[string]interface{}{
"name": tenant.Name,
"slug": tenant.Slug,
})
t.config.Analytics.Enqueue(
"tenant:create",
user.ID,
&tenant.ID,
nil,
)
return gen.TenantCreate200JSONResponse(
*transformers.ToTenant(tenant),
), nil

View File

@@ -53,6 +53,12 @@ func (t *TenantService) TenantInviteCreate(ctx echo.Context, request gen.TenantI
return nil, err
}
t.config.Analytics.Enqueue("user-invite:create",
user.ID,
&invite.TenantID,
nil,
)
return gen.TenantInviteCreate201JSONResponse(
*transformers.ToTenantInviteLink(invite),
), nil

View File

@@ -81,5 +81,12 @@ func (u *UserService) TenantInviteAccept(ctx echo.Context, request gen.TenantInv
return nil, err
}
u.config.Analytics.Enqueue(
"user-invite:reject",
user.ID,
&invite.TenantID,
nil,
)
return nil, nil
}

View File

@@ -70,6 +70,16 @@ func (u *UserService) UserCreate(ctx echo.Context, request gen.UserCreateRequest
return nil, err
}
u.config.Analytics.Enqueue(
"user:create",
user.ID,
nil,
map[string]interface{}{
"email": request.Body.Email,
"name": request.Body.Name,
},
)
return gen.UserCreate200JSONResponse(
*transformers.ToUser(user, false),
), nil

View File

@@ -25,7 +25,19 @@ func (u *UserService) UserGetCurrent(ctx echo.Context, request gen.UserGetCurren
hasPass = true
}
transformedUser := transformers.ToUser(user, hasPass)
u.config.Analytics.Enqueue(
"user:current",
user.ID,
nil,
map[string]interface{}{
"email": user.Email,
"name": transformedUser.Name,
},
)
return gen.UserGetCurrent200JSONResponse(
*transformers.ToUser(user, hasPass),
*transformedUser,
), nil
}

View File

@@ -62,5 +62,12 @@ func (u *UserService) TenantInviteReject(ctx echo.Context, request gen.TenantInv
return nil, err
}
u.config.Analytics.Enqueue(
"user-invite:accept",
user.ID,
&invite.TenantID,
nil,
)
return nil, nil
}

1
go.mod
View File

@@ -73,6 +73,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/posthog/posthog-go v0.0.0-20240327112532-87b23fe11103 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect

2
go.sum
View File

@@ -212,6 +212,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posthog/posthog-go v0.0.0-20240327112532-87b23fe11103 h1:YEWdfKVtz5Db85b8RLIZ1IY3PLSB1fW49hvK2yIL6JU=
github.com/posthog/posthog-go v0.0.0-20240327112532-87b23fe11103/go.mod h1:QjlpryJtfYLrZF2GUkAhejH4E7WlDbdKkvOi5hLmkdg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo=
github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc=

View File

@@ -32,6 +32,8 @@ import (
"github.com/hatchet-dev/hatchet/internal/repository/prisma/db"
"github.com/hatchet-dev/hatchet/internal/services/ingestor"
"github.com/hatchet-dev/hatchet/internal/validator"
"github.com/hatchet-dev/hatchet/pkg/analytics"
"github.com/hatchet-dev/hatchet/pkg/analytics/posthog"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/errors"
"github.com/hatchet-dev/hatchet/pkg/errors/sentry"
@@ -216,6 +218,21 @@ func GetServerConfigFromConfigfile(dc *database.Config, cf *server.ServerConfigF
alerter = errors.NoOpAlerter{}
}
var analyticsEmitter analytics.Analytics
if cf.Analytics.Posthog.Enabled {
analyticsEmitter, err = posthog.NewPosthogAnalytics(&posthog.PosthogAnalyticsOpts{
ApiKey: cf.Analytics.Posthog.ApiKey,
Endpoint: cf.Analytics.Posthog.Endpoint,
})
if err != nil {
return nil, nil, fmt.Errorf("could not create posthog analytics: %w", err)
}
} else {
analyticsEmitter = analytics.NoOpAnalytics{} // TODO
}
auth := server.AuthConfig{
ConfigFile: cf.Auth,
}
@@ -347,6 +364,7 @@ func GetServerConfigFromConfigfile(dc *database.Config, cf *server.ServerConfigF
return cleanup, &server.ServerConfig{
Alerter: alerter,
Analytics: analyticsEmitter,
Runtime: cf.Runtime,
Auth: auth,
Encryption: encryptionSvc,

View File

@@ -17,6 +17,7 @@ import (
"github.com/hatchet-dev/hatchet/internal/msgqueue"
"github.com/hatchet-dev/hatchet/internal/services/ingestor"
"github.com/hatchet-dev/hatchet/internal/validator"
"github.com/hatchet-dev/hatchet/pkg/analytics"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/errors"
)
@@ -26,6 +27,8 @@ type ServerConfigFile struct {
Alerting AlertingConfigFile `mapstructure:"alerting" json:"alerting,omitempty"`
Analytics AnalyticsConfigFile `mapstructure:"analytics" json:"analytics,omitempty"`
Encryption EncryptionConfigFile `mapstructure:"encryption" json:"encryption,omitempty"`
Runtime ConfigFileRuntime `mapstructure:"runtime" json:"runtime,omitempty"`
@@ -86,6 +89,21 @@ type SentryConfigFile struct {
Environment string `mapstructure:"environment" json:"environment,omitempty" default:"development"`
}
type AnalyticsConfigFile struct {
Posthog PosthogConfigFile `mapstructure:"posthog" json:"posthog,omitempty"`
}
type PosthogConfigFile struct {
// Enabled controls whether the Posthog service is enabled for this Hatchet instance.
Enabled bool `mapstructure:"enabled" json:"enabled,omitempty"`
// APIKey is the API key for the Posthog instance
ApiKey string `mapstructure:"apiKey" json:"apiKey,omitempty"`
// Endpoint is the endpoint for the Posthog instance
Endpoint string `mapstructure:"endpoint" json:"endpoint,omitempty"`
}
// Encryption options
type EncryptionConfigFile struct {
// MasterKeyset is the raw master keyset for the instance. This should be a base64-encoded JSON string. You must set
@@ -213,6 +231,8 @@ type ServerConfig struct {
Alerter errors.Alerter
Analytics analytics.Analytics
Encryption encryption.EncryptionService
Runtime ConfigFileRuntime
@@ -267,6 +287,11 @@ func BindAllEnv(v *viper.Viper) {
_ = v.BindEnv("alerting.sentry.dsn", "SERVER_ALERTING_SENTRY_DSN")
_ = v.BindEnv("alerting.sentry.environment", "SERVER_ALERTING_SENTRY_ENVIRONMENT")
// analytics options
_ = v.BindEnv("analytics.posthog.enabled", "SERVER_ANALYTICS_POSTHOG_ENABLED")
_ = v.BindEnv("analytics.posthog.apiKey", "SERVER_ANALYTICS_POSTHOG_API_KEY")
_ = v.BindEnv("analytics.posthog.endpoint", "SERVER_ANALYTICS_POSTHOG_ENDPOINT")
// encryption options
_ = v.BindEnv("encryption.masterKeyset", "SERVER_ENCRYPTION_MASTER_KEYSET")
_ = v.BindEnv("encryption.masterKeysetFile", "SERVER_ENCRYPTION_MASTER_KEYSET_FILE")

View File

@@ -28,6 +28,7 @@ import (
"github.com/hatchet-dev/hatchet/internal/services/grpc/middleware"
"github.com/hatchet-dev/hatchet/internal/services/ingestor"
eventcontracts "github.com/hatchet-dev/hatchet/internal/services/ingestor/contracts"
"github.com/hatchet-dev/hatchet/pkg/analytics"
"github.com/hatchet-dev/hatchet/pkg/errors"
)
@@ -38,6 +39,7 @@ type Server struct {
l *zerolog.Logger
a errors.Alerter
analytics analytics.Analytics
port int
bindAddress string
@@ -55,6 +57,7 @@ type ServerOpts struct {
config *server.ServerConfig
l *zerolog.Logger
a errors.Alerter
analytics analytics.Analytics
port int
bindAddress string
ingestor ingestor.Ingestor
@@ -67,10 +70,11 @@ type ServerOpts struct {
func defaultServerOpts() *ServerOpts {
logger := logger.NewDefaultLogger("grpc")
a := errors.NoOpAlerter{}
analytics := analytics.NoOpAnalytics{}
return &ServerOpts{
l: &logger,
a: a,
analytics: analytics,
port: 7070,
bindAddress: "127.0.0.1",
insecure: false,
@@ -89,6 +93,12 @@ func WithAlerter(a errors.Alerter) ServerOpt {
}
}
func WithAnalytics(a analytics.Analytics) ServerOpt {
return func(opts *ServerOpts) {
opts.analytics = a
}
}
func WithBindAddress(bindAddress string) ServerOpt {
return func(opts *ServerOpts) {
opts.bindAddress = bindAddress
@@ -158,6 +168,7 @@ func NewServer(fs ...ServerOpt) (*Server, error) {
return &Server{
l: opts.l,
a: opts.a,
analytics: opts.analytics,
config: opts.config,
port: opts.port,
bindAddress: opts.bindAddress,

View File

@@ -0,0 +1,12 @@
package analytics
type Analytics interface {
Enqueue(event string, userId string, tenantId *string, data map[string]interface{})
Tenant(tenantId string, data map[string]interface{})
}
type NoOpAnalytics struct{}
func (a NoOpAnalytics) Enqueue(event string, userId string, tenantId *string, data map[string]interface{}) {
}
func (a NoOpAnalytics) Tenant(tenantId string, data map[string]interface{}) {}

View File

@@ -0,0 +1,65 @@
package posthog
import (
"fmt"
"github.com/posthog/posthog-go"
)
type PosthogAnalytics struct {
client *posthog.Client
}
type PosthogAnalyticsOpts struct {
ApiKey string
Endpoint string
}
func NewPosthogAnalytics(opts *PosthogAnalyticsOpts) (*PosthogAnalytics, error) {
if opts.ApiKey == "" || opts.Endpoint == "" {
return nil, fmt.Errorf("api key and endpoint are required if posthog is enabled")
}
phClient, err := posthog.NewWithConfig(
opts.ApiKey,
posthog.Config{
Endpoint: opts.Endpoint,
},
)
if err != nil {
return nil, fmt.Errorf("failed to create posthog client: %w", err)
}
return &PosthogAnalytics{
client: &phClient,
}, nil
}
func (p *PosthogAnalytics) Enqueue(event string, userId string, tenantId *string, data map[string]interface{}) {
var group posthog.Groups
if tenantId != nil {
group = posthog.NewGroups().Set("tenant", *tenantId)
}
var _ = (*p.client).Enqueue(posthog.Capture{
DistinctId: userId,
Event: event,
Properties: map[string]interface{}{
"$set": data,
},
Groups: group,
})
}
func (p *PosthogAnalytics) Tenant(tenantId string, data map[string]interface{}) {
var _ = (*p.client).Enqueue(posthog.GroupIdentify{
Type: "tenant",
Key: tenantId,
Properties: map[string]interface{}{
"$set": data,
},
})
}