Merge branch 'feat/telemetry'

This commit is contained in:
Benjamin
2025-12-22 22:17:03 +01:00
24 changed files with 237 additions and 17 deletions
+2 -1
View File
@@ -68,4 +68,5 @@ ACKIFY_ED25519_PRIVATE_KEY=your_base64_encoded_ed25519_private_key
# ACKIFY_ONLY_ADMIN_CAN_CREATE=false
# Server Configuration
ACKIFY_LISTEN_ADDR=:8080
ACKIFY_LISTEN_ADDR=:8080
ACKIFY_TELEMETRY=false
+2 -1
View File
@@ -42,7 +42,8 @@ func main() {
logger.Logger.Info("Starting Ackify Community Edition",
"version", Version,
"commit", Commit,
"build_date", BuildDate)
"build_date", BuildDate,
"telemetry", cfg.Telemetry)
// Initialize DB
db, err := database.InitDB(ctx, database.Config{DSN: cfg.Database.DSN})
@@ -468,6 +468,15 @@ func (s *DocumentService) Count(ctx context.Context, searchQuery string) (int, e
return s.repo.Count(ctx, searchQuery)
}
// CountDocs returns the current count of documents
func (s *DocumentService) CountDocs(ctx context.Context) int {
c, err := s.repo.Count(ctx, "")
if err != nil {
c = 0
}
return c
}
// GetByDocID retrieves a document by its ID
func (s *DocumentService) GetByDocID(ctx context.Context, docID string) (*models.Document, error) {
return s.repo.GetByDocID(ctx, docID)
@@ -26,6 +26,7 @@ type asyncReminderRepository interface {
LogReminder(ctx context.Context, log *models.ReminderLog) error
GetReminderHistory(ctx context.Context, docID string) ([]*models.ReminderLog, error)
GetReminderStats(ctx context.Context, docID string) (*models.ReminderStats, error)
Count(ctx context.Context) (int, error)
}
// asyncMagicLinkService defines magic link operations for async reminders
@@ -305,3 +306,12 @@ func (s *ReminderAsyncService) SendReminders(
) (*models.ReminderSendResult, error) {
return s.SendRemindersAsync(ctx, docID, sentBy, specificEmails, docURL, locale)
}
// CountSent returns the number of sent reminders for a document
func (s *ReminderAsyncService) CountSent(ctx context.Context) int {
c, err := s.reminderRepo.Count(ctx)
if err != nil {
return 0
}
return c
}
@@ -24,6 +24,7 @@ type repository interface {
GetLastSignature(ctx context.Context, docID string) (*models.Signature, error)
GetAllSignaturesOrdered(ctx context.Context) ([]*models.Signature, error)
UpdatePrevHash(ctx context.Context, id int64, prevHash *string) error
Count(ctx context.Context) (int, error)
}
type cryptoSigner interface {
@@ -442,3 +443,12 @@ func (s *SignatureService) verifyDocumentIntegrity(ctx context.Context, doc *mod
return nil
}
// CountSigns returns the current count of signatures in the database
func (s *SignatureService) CountSigns(ctx context.Context) int {
c, err := s.repo.Count(ctx)
if err != nil {
c = 0
}
return c
}
@@ -71,6 +71,10 @@ func (m *mockSignatureRepository) UpdatePrevHash(ctx context.Context, id int64,
return nil
}
func (m *mockSignatureRepository) Count(ctx context.Context) (int, error) {
return 0, nil
}
// mockCryptoSigner for testing
type mockCryptoSigner struct{}
@@ -150,6 +150,10 @@ func (f *fakeRepository) UpdatePrevHash(_ context.Context, id int64, prevHash *s
return nil
}
func (f *fakeRepository) Count(_ context.Context) (int, error) {
return len(f.allSignatures), nil
}
type fakeCryptoSigner struct {
shouldFail bool
}
@@ -0,0 +1 @@
package telemetry
@@ -17,6 +17,7 @@ type webhookRepository interface {
GetByID(ctx context.Context, id int64) (*models.Webhook, error)
List(ctx context.Context, limit, offset int) ([]*models.Webhook, error)
ListActiveByEvent(ctx context.Context, event string) ([]*models.Webhook, error)
Count(ctx context.Context) (int, error)
}
// webhookDeliveryRepository defines webhook delivery operations
@@ -87,3 +88,12 @@ func (s *WebhookService) ListDeliveries(ctx context.Context, webhookID int64, li
func (s *WebhookService) EnqueueDelivery(ctx context.Context, input models.WebhookDeliveryInput) (*models.WebhookDelivery, error) {
return s.deliveryRepo.Enqueue(ctx, input)
}
// CountWebhooks returns the total number of webhooks
func (s *WebhookService) CountWebhooks(ctx context.Context) int {
c, err := s.webhookRepo.Count(ctx)
if err != nil {
c = 0
}
return c
}
@@ -443,7 +443,6 @@ func (r *DocumentRepository) Search(ctx context.Context, query string, limit, of
}
// Count returns the total number of documents matching the optional search query (excluding soft-deleted)
// RLS policy automatically filters by tenant_id
func (r *DocumentRepository) Count(ctx context.Context, searchQuery string) (int, error) {
var query string
var args []interface{}
@@ -190,3 +190,14 @@ func (r *ReminderRepository) GetReminderStats(ctx context.Context, docID string)
return stats, nil
}
// Count returns the number of sent reminders in the database
func (r *ReminderRepository) Count(ctx context.Context) (int, error) {
query := `SELECT COUNT(*) FROM reminder_logs WHERE status = 'sent'`
var count int
err := dbctx.GetQuerier(ctx, r.db).QueryRowContext(ctx, query).Scan(&count)
if err != nil {
return 0, fmt.Errorf("failed to count sent reminders count: %w", err)
}
return count, nil
}
@@ -320,3 +320,14 @@ func (r *SignatureRepository) UpdatePrevHash(ctx context.Context, id int64, prev
}
return nil
}
// Count returns the total number of unique signers (distinct email addresses)
func (r *SignatureRepository) Count(ctx context.Context) (int, error) {
query := `SELECT COUNT(DISTINCT user_email) FROM signatures`
var count int
err := dbctx.GetQuerier(ctx, r.db).QueryRowContext(ctx, query).Scan(&count)
if err != nil {
return 0, fmt.Errorf("failed to count unique signers: %w", err)
}
return count, nil
}
@@ -222,3 +222,14 @@ func (r *WebhookRepository) ListActiveByEvent(ctx context.Context, event string)
}
return res, nil
}
// Count returns the total number of unique webhooks (distinct target URLs)
func (r *WebhookRepository) Count(ctx context.Context) (int, error) {
query := `SELECT COUNT(DISTINCT target_url) FROM webhooks WHERE active = TRUE`
var count int
err := dbctx.GetQuerier(ctx, r.db).QueryRowContext(ctx, query).Scan(&count)
if err != nil {
return 0, fmt.Errorf("failed to count unique webhooks: %w", err)
}
return count, nil
}
+17 -13
View File
@@ -12,14 +12,15 @@ import (
)
type Config struct {
App AppConfig
Server ServerConfig
Database DatabaseConfig
Checksum ChecksumConfig
Auth AuthConfig
OAuth OAuthConfig
Mail MailConfig
Logger LoggerConfig
App AppConfig
Server ServerConfig
Database DatabaseConfig
Checksum ChecksumConfig
Auth AuthConfig
OAuth OAuthConfig
Mail MailConfig
Logger LoggerConfig
Telemetry bool
}
type AuthConfig struct {
@@ -63,11 +64,6 @@ type ServerConfig struct {
ListenAddr string
}
type LoggerConfig struct {
Level string
Format string // "classic" or "json"
}
type MailConfig struct {
Host string
Port int
@@ -93,6 +89,11 @@ type ChecksumConfig struct {
InsecureSkipVerify bool // For testing only - DO NOT use in production
}
type LoggerConfig struct {
Level string
Format string // "classic" or "json"
}
// Load loads configuration from environment variables
func Load() (*Config, error) {
config := &Config{}
@@ -256,6 +257,9 @@ func Load() (*Config, error) {
// CSV import configuration
config.App.ImportMaxSigners = getEnvInt("ACKIFY_IMPORT_MAX_SIGNERS", 500)
// Telemetry configuration
config.Telemetry = getEnv("ACKIFY_TELEMETRY", "false") != "false" && getEnv("DO_NOT_TRACK", "") != "1"
// Validation: At least one authentication method must be enabled
if !config.Auth.OAuthEnabled && !config.Auth.MagicLinkEnabled {
return nil, fmt.Errorf("at least one authentication method must be enabled: set ACKIFY_OAUTH_CLIENT_ID/CLIENT_SECRET for OAuth or ACKIFY_MAIL_HOST for MagicLink")
+31 -1
View File
@@ -27,6 +27,8 @@ import (
"github.com/btouchard/ackify-ce/backend/internal/presentation/handlers"
"github.com/btouchard/ackify-ce/backend/pkg/crypto"
"github.com/btouchard/ackify-ce/backend/pkg/logger"
sdk "github.com/btouchard/shm/sdk/golang"
)
// Server represents the HTTP server with all its dependencies.
@@ -86,7 +88,7 @@ type ServerBuilder struct {
magicLinkEnabled bool
}
// NewServerBuilder creates a new server builder with required configuration.
// NewServerBuilder creates a new server builder with the required configuration.
func NewServerBuilder(cfg *config.Config, frontend embed.FS, version string) *ServerBuilder {
return &ServerBuilder{
cfg: cfg,
@@ -214,6 +216,11 @@ func (b *ServerBuilder) Build(ctx context.Context) (*Server, error) {
// Create repositories
repos := b.createRepositories()
// Initialize Telemetry if is enabled
if err := b.initializeTelemetry(ctx); err != nil {
return nil, err
}
// Initialize workers and services
whPublisher, whWorker, err := b.initializeWebhookSystem(repos)
if err != nil {
@@ -348,6 +355,29 @@ func (b *ServerBuilder) createRepositories() *repositories {
}
}
func (b *ServerBuilder) initializeTelemetry(ctx context.Context) error {
telemetry, err := sdk.New(sdk.Config{
ServerURL: "https://metrics.kolapsis.com",
AppName: "Ackify",
AppVersion: b.version,
Environment: "production",
Enabled: b.cfg.Telemetry,
})
if err != nil {
return err
}
telemetry.SetProvider(func() map[string]interface{} {
return map[string]interface{}{
"documents": b.documentService.CountDocs(ctx),
"confirmations": b.signatureService.CountSigns(ctx),
"webhooks": b.webhookService.CountWebhooks(ctx),
"reminds_sent": b.reminderService.CountSent(ctx),
}
})
go telemetry.Start(context.Background())
return nil
}
// initializeWebhookSystem initializes webhook publisher and worker.
func (b *ServerBuilder) initializeWebhookSystem(repos *repositories) (*services.WebhookPublisher, *webhook.Worker, error) {
whPublisher := services.NewWebhookPublisher(repos.webhook, repos.webhookDelivery)
+1
View File
@@ -52,6 +52,7 @@ services:
ACKIFY_AUTH_RATE_LIMIT: "1000"
ACKIFY_DOCUMENT_RATE_LIMIT: "1000"
ACKIFY_GENERAL_RATE_LIMIT: "1000"
ACKIFY_TELEMETRY: false
depends_on:
ackify-migrate:
condition: service_completed_successfully
+1
View File
@@ -45,6 +45,7 @@ services:
ACKIFY_MAIL_STARTTLS: "false"
ACKIFY_MAIL_FROM: "${ACKIFY_MAIL_FROM:-noreply@ackify.local}"
ACKIFY_MAIL_FROM_NAME: "${ACKIFY_MAIL_FROM_NAME:-Ackify}"
ACKIFY_TELEMETRY: "${ACKIFY_TELEMETRY:-false}"
depends_on:
ackify-migrate:
condition: service_completed_successfully
+1
View File
@@ -3,6 +3,7 @@ module github.com/btouchard/ackify-ce
go 1.24.5
require (
github.com/btouchard/shm v1.2.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-mail/mail/v2 v2.3.0
github.com/golang-migrate/migrate/v4 v4.19.0
+2
View File
@@ -2,6 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/btouchard/shm v1.2.0 h1:4KsVAWhbvvLnTKEagvbt47DxwwBq8PvglW8Tlsg+bFc=
github.com/btouchard/shm v1.2.0/go.mod h1:9+E/t1eveTZwmXGnsTqBn7HeckuQfp7OyCHLE64uS/A=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+10
View File
@@ -110,6 +110,16 @@ ACKIFY_OAUTH_CLIENT_SECRET=your_oauth_client_secret
# When enabled, only admins can create new documents
# ACKIFY_ONLY_ADMIN_CAN_CREATE=false
# ==========================================
# Telemetry Configuration
# ==========================================
# Anonymous usage metrics to help improve Ackify
# - GDPR compliant (no personal data collected)
# - Business metrics only: documents, signatures, webhooks, reminders count
# - Non-intrusive (background collection)
# Set to true to help us improve Ackify!
ACKIFY_TELEMETRY=false
# ==========================================
# CONFIGURATION INSTRUCTIONS
# ==========================================
+35
View File
@@ -144,6 +144,40 @@ MagicLink provides passwordless authentication via email. Users receive a secure
- Internal applications where email domain is trusted
- Combination with OAuth for flexible authentication
## Anonymous Telemetry
Ackify can collect anonymous usage metrics to help improve the project.
### What is collected
**Business metrics only:**
- Number of documents created
- Number of signatures/confirmations
- Number of webhooks configured
- Number of email reminders sent
### What is NOT collected
- No personal data
- No user information (names, emails, IPs)
- No document content
- No authentication details
### Privacy
- **GDPR compliant** - No personal data is ever collected
- **Non-intrusive** - Runs in background, no impact on performance
- **Opt-in** - Disabled by default, you choose to enable it
### Configuration
```env
# Enable anonymous telemetry (default: false)
ACKIFY_TELEMETRY=true
```
We encourage you to enable telemetry to help us improve Ackify for everyone!
## SMTP Configuration
SMTP is used for:
@@ -193,6 +227,7 @@ ADMIN_EMAILS=admin@your-domain.com
- `MAIL_*` - SMTP configuration for email features
- `AUTH_MAGICLINK_ENABLED` - Force enable/disable MagicLink
- `ONLY_ADMIN_CAN_CREATE` - Restrict document creation to admins only (default: false)
- `ACKIFY_TELEMETRY` - Enable anonymous usage metrics (default: false)
## Troubleshooting
+1
View File
@@ -68,6 +68,7 @@ services:
ACKIFY_CHECKSUM_MAX_REDIRECTS: "${ACKIFY_CHECKSUM_MAX_REDIRECTS:-3}"
ACKIFY_CHECKSUM_ALLOWED_TYPES: "${ACKIFY_CHECKSUM_ALLOWED_TYPES:-}"
ACKIFY_IMPORT_MAX_SIGNERS: "${ACKIFY_IMPORT_MAX_SIGNERS:-500}"
ACKIFY_TELEMETRY: "${ACKIFY_TELEMETRY:-false}"
depends_on:
ackify-migrate:
condition: service_completed_successfully
+1
View File
@@ -68,6 +68,7 @@ services:
ACKIFY_CHECKSUM_MAX_REDIRECTS: "${ACKIFY_CHECKSUM_MAX_REDIRECTS:-3}"
ACKIFY_CHECKSUM_ALLOWED_TYPES: "${ACKIFY_CHECKSUM_ALLOWED_TYPES:-}"
ACKIFY_IMPORT_MAX_SIGNERS: "${ACKIFY_IMPORT_MAX_SIGNERS:-500}"
ACKIFY_TELEMETRY: "${ACKIFY_TELEMETRY:-false}"
depends_on:
ackify-migrate:
condition: service_completed_successfully
+52
View File
@@ -306,6 +306,41 @@ if [ "$ENABLE_OAUTH" = false ] && [ "$ENABLE_MAGICLINK" = false ]; then
exit 1
fi
# ==========================================
# Telemetry Configuration
# ==========================================
print_header "📊 Anonymous Telemetry"
echo ""
print_info "Ackify can collect anonymous usage metrics to help improve the project."
echo ""
print_info "What is collected (business metrics only):"
print_info " - Number of documents created"
print_info " - Number of signatures/confirmations"
print_info " - Number of webhooks configured"
print_info " - Number of email reminders sent"
echo ""
print_info "What is NOT collected:"
print_info " - No personal data"
print_info " - No user information"
print_info " - No document content"
print_info " - No email addresses"
print_info " - No IP addresses"
echo ""
print_info "This telemetry is:"
print_success " ✓ GDPR compliant"
print_success " ✓ Non-intrusive (background only)"
print_success " ✓ Helps us improve Ackify for everyone"
echo ""
ENABLE_TELEMETRY=false
if prompt_yes_no "Enable anonymous telemetry to help improve Ackify?" "y"; then
ENABLE_TELEMETRY=true
print_success "Thank you for helping improve Ackify!"
else
print_info "Telemetry disabled. You can enable it later in .env (ACKIFY_TELEMETRY=true)"
fi
echo ""
# ==========================================
# Admin Configuration
# ==========================================
@@ -498,6 +533,16 @@ APP_DNS=${APP_DNS}
EOF
fi
# Telemetry configuration
cat >> .env <<EOF
# ==========================================
# Telemetry Configuration
# ==========================================
# Anonymous usage metrics (GDPR compliant, no personal data)
ACKIFY_TELEMETRY=${ENABLE_TELEMETRY}
EOF
print_success ".env file created successfully"
echo ""
@@ -548,6 +593,13 @@ else
fi
echo ""
if [ "$ENABLE_TELEMETRY" = true ]; then
print_success "Telemetry: Enabled (thank you!)"
else
print_info "Telemetry: Disabled"
fi
echo ""
# ==========================================
# Next Steps
# ==========================================