Files
pulse/internal/api/reporting_activity.go
rcourtman a224a1a195 Measure aggregate reporting outcomes and repeat use
Record successful response writes, scheduled generation, file saves and SMTP
outcomes in a durable content-free tally. Distinct active days include manual
exports, while saved-schedule periods deduplicate retries and monthly reuse.

Publish bounded scalars through the existing opt-out-controlled heartbeat and
report missing coverage explicitly. Preserve pricing and customer entitlements.
2026-10-17 21:10:17 +01:00

81 lines
2.8 KiB
Go

package api
import (
"context"
"crypto/sha256"
"encoding/hex"
"net/http"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rs/zerolog/log"
)
type reportingActivityRecorder interface {
RecordReportingActivity(config.ReportingActivityEvent) error
}
func (h *ReportingHandlers) recordReportingActivity(event config.ReportingActivityEvent) {
if h == nil || mock.IsMockEnabled() {
return
}
recorder, ok := h.settingsStore.(reportingActivityRecorder)
if !ok {
return
}
if err := recorder.RecordReportingActivity(event); err != nil {
// No report, scope, recipient, schedule or error content enters this log.
log.Debug().Msg("Unable to retain aggregate reporting activity")
}
}
func (h *ReportingHandlers) recordReportingDownload(r *http.Request, kind string, count int, err error, expected int) {
if r == nil || r.Method == http.MethodHead || r.Context().Err() != nil || err != nil || count != expected {
return
}
h.recordReportingActivity(config.ReportingActivityEvent{At: time.Now().UTC(), Kind: kind, Event: "generated"})
}
func reportingActivityDigest(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
func reportingActivityPeriod(schedule config.ReportSchedule, start, end time.Time, now time.Time) string {
if start.IsZero() || end.IsZero() {
anchor := now
if schedule.RunWindowEnd != nil {
anchor = *schedule.RunWindowEnd
}
var err error
start, end, err = reportScheduleWindow(schedule, anchor, schedule.RunManual)
if err != nil {
return ""
}
}
location, err := reportScheduleLocation(schedule.Cadence.Timezone)
if err != nil || !end.After(start) {
return ""
}
// Day boundaries describe the reporting window. A retry or same-day rerun
// cannot become repeat use merely because its clock time changed.
return reportingActivityDigest(start.In(location).Format(time.DateOnly) + "\x00" + end.In(location).Format(time.DateOnly))
}
func (h *ReportingHandlers) recordReportingSuccessfulPeriod(ctx context.Context, schedule config.ReportSchedule, period string, at time.Time) {
if schedule.ID == "" || period == "" || schedule.LastRunStatus != config.ReportScheduleLastRunOK {
return
}
key := reportingActivityDigest(GetOrgID(ctx) + "\x00" + schedule.Kind + "\x00" + schedule.ID)
h.recordReportingActivity(config.ReportingActivityEvent{At: at, Kind: schedule.Kind, Event: "successful_period", ScheduleKey: key, PeriodKey: period})
}
func (h *ReportingHandlers) recordReportingDeliveryFailure(kind string, status string, at time.Time) {
event := "delivery_failed"
if status == "unconfirmed" {
event = "delivery_unconfirmed"
}
h.recordReportingActivity(config.ReportingActivityEvent{At: at, Kind: kind, Event: event})
}