diff --git a/backend/go.mod b/backend/go.mod index fd12ab08..4dd4ef0f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/caarlos0/env/v11 v11.3.1 + github.com/cenkalti/backoff/v5 v5.0.2 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/disintegration/imaging v1.6.2 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 diff --git a/backend/go.sum b/backend/go.sum index edc90309..7042fe85 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -17,6 +17,8 @@ github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5m github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= diff --git a/backend/internal/job/analytics_job.go b/backend/internal/job/analytics_job.go index f3508cee..53895cae 100644 --- a/backend/internal/job/analytics_job.go +++ b/backend/internal/job/analytics_job.go @@ -6,6 +6,9 @@ import ( "encoding/json" "fmt" "net/http" + "time" + + backoff "github.com/cenkalti/backoff/v5" "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/service" @@ -14,7 +17,15 @@ import ( const heartbeatUrl = "https://analytics.pocket-id.org/heartbeat" func (s *Scheduler) RegisterAnalyticsJob(ctx context.Context, appConfig *service.AppConfigService, httpClient *http.Client) error { - jobs := &AnalyticsJob{appConfig: appConfig, httpClient: httpClient} + // Skip if analytics are disabled or not in production environment + if common.EnvConfig.AnalyticsDisabled || common.EnvConfig.AppEnv != "production" { + return nil + } + + jobs := &AnalyticsJob{ + appConfig: appConfig, + httpClient: httpClient, + } return s.registerJob(ctx, "SendHeartbeat", "0 0 * * *", jobs.sendHeartbeat, true) } @@ -24,38 +35,50 @@ type AnalyticsJob struct { } // sendHeartbeat sends a heartbeat to the analytics service -func (j *AnalyticsJob) sendHeartbeat(ctx context.Context) error { +func (j *AnalyticsJob) sendHeartbeat(parentCtx context.Context) error { // Skip if analytics are disabled or not in production environment if common.EnvConfig.AnalyticsDisabled || common.EnvConfig.AppEnv != "production" { return nil } - body := struct { + body, err := json.Marshal(struct { Version string `json:"version"` InstanceID string `json:"instance_id"` }{ Version: common.Version, InstanceID: j.appConfig.GetDbConfig().InstanceID.Value, - } - bodyBytes, err := json.Marshal(body) + }) if err != nil { return fmt.Errorf("failed to marshal heartbeat body: %w", err) } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, heartbeatUrl, bytes.NewBuffer(bodyBytes)) + _, err = backoff.Retry( + parentCtx, + func() (struct{}, error) { + ctx, cancel := context.WithTimeout(parentCtx, 20*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, heartbeatUrl, bytes.NewReader(body)) + if err != nil { + return struct{}{}, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := j.httpClient.Do(req) + if err != nil { + return struct{}{}, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return struct{}{}, fmt.Errorf("request failed with status code: %d", resp.StatusCode) + } + return struct{}{}, nil + }, + backoff.WithBackOff(backoff.NewExponentialBackOff()), + backoff.WithMaxTries(3), + ) + if err != nil { - return fmt.Errorf("failed to create heartbeat request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - resp, err := j.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to send heartbeat request: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("heartbeat request failed with status code: %d", resp.StatusCode) + return fmt.Errorf("heartbeat request failed: %w", err) } return nil - }