Skip to content

Commit

Permalink
Add option to send email reports
Browse files Browse the repository at this point in the history
Various bits and pieces; very much WIP.
  • Loading branch information
arp242 committed May 27, 2020
1 parent f9abf0b commit 1b08fcd
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 0 deletions.
1 change: 1 addition & 0 deletions cron/cron.go
Expand Up @@ -29,6 +29,7 @@ var tasks = []task{
{goatcounter.Salts.Refresh, 1 * time.Hour},
{clearSessions, 1 * time.Minute},
{oldExports, 1 * time.Hour},
{emailReports, 1 * time.Hour},
}

var (
Expand Down
94 changes: 94 additions & 0 deletions cron/email_reports.go
@@ -0,0 +1,94 @@
// Copyright © 2019 Martin Tournoij <martin@arp242.net>
// This file is part of GoatCounter and published under the terms of the EUPL
// v1.2, which can be found in the LICENSE file or at http://eupl12.zgo.at

package cron

import (
"context"
"fmt"
"time"

"zgo.at/blackmail"
"zgo.at/goatcounter"
"zgo.at/goatcounter/cfg"
"zgo.at/zdb"
"zgo.at/zhttp"
"zgo.at/zlog"
)

func emailReports(ctx context.Context) error {
db := zdb.MustGet(ctx)

var ids []int64
err := db.SelectContext(ctx, &ids,
`select id from sites where settings->>'email_reports'::varchar != '0'`)
if err != nil {
return fmt.Errorf("cron.emailReports get sites: %w", err)
}

var sites goatcounter.Sites
err = sites.ListIDs(ctx, ids...)
if err != nil {
return fmt.Errorf("cron.emailReports: %w", err)
}

// Note: maybe pool subsites in one email?
for _, s := range sites {
text, html, err := report(ctx, s)
if err != nil {
zlog.Field("site", s.ID).Errorf("cron.emailReports: %w", err)
continue
}

blackmail.Send("Report",
blackmail.From("GoatCounter report", cfg.EmailFrom),
blackmail.To("TODO@TODO.TODO"),
blackmail.BodyText(text),
blackmail.BodyHTML(html))
}

return nil
}

type templateArgs struct {
Site goatcounter.Site
PeriodName string
Pages goatcounter.HitStats
}

func report(ctx context.Context, s goatcounter.Site) ([]byte, []byte, error) {
ctx = goatcounter.WithSite(ctx, &s)

pn := map[int]string{
-1: "first-time",
1: "weekly",
2: "biweekly",
3: "monthly",
}[s.Settings.EmailReports.Int()]

start := goatcounter.Now().Add(-7 * 24 * time.Hour)
end := goatcounter.Now().Add(-14 * 24 * time.Hour)

var pages goatcounter.HitStats
_, _, _, _, _, _, err := pages.List(ctx, start, end, "", nil, true)
if err != nil {
return nil, nil, fmt.Errorf("cron.report: %w", err)
}

args := templateArgs{
Site: s,
PeriodName: pn,
Pages: pages,
}

text, err := zhttp.ExecuteTpl("email_report.gotxt", args)
if err != nil {
return nil, nil, fmt.Errorf("cron.report text: %w", err)
}
html, err := zhttp.ExecuteTpl("email_report.gohtml", args)
if err != nil {
return nil, nil, fmt.Errorf("cron.report html: %w", err)
}
return text, html, nil
}
46 changes: 46 additions & 0 deletions pack/pack.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions site.go
Expand Up @@ -13,10 +13,12 @@ import (
"strings"
"time"

"github.com/jmoiron/sqlx"
"zgo.at/errors"
"zgo.at/goatcounter/cfg"
"zgo.at/guru"
"zgo.at/tz"
"zgo.at/utils/intutil"
"zgo.at/utils/jsonutil"
"zgo.at/zdb"
"zgo.at/zhttp"
Expand Down Expand Up @@ -70,6 +72,8 @@ type SiteSettings struct {
DateFormat string `json:"date_format"`
NumberFormat rune `json:"number_format"`
DataRetention int `json:"data_retention"`
EmailReports intutil.Int `json:"email_reports"`
EmailReportsCc string `json:"email_reports"`
IgnoreIPs zdb.Strings `json:"ignore_ips"`
Timezone *tz.Zone `json:"timezone"`
Campaigns zdb.Strings `json:"campaigns"`
Expand All @@ -96,11 +100,24 @@ func (ss *SiteSettings) Scan(v interface{}) error {
}
}

// Settings.EmailReport
const (
EmailReportOnce = -1 // Email once after 2 weeks; for new sites.
EmailReportNever = 0
EmailReportDaily = 1
EmailReportWeekly = 2
EmailReportBieekly = 2
EmailReportMonthly = 3
)

var EmailReports = []int{-1, 0, 1, 2, 3}

// Defaults sets fields to default values, unless they're already set.
func (s *Site) Defaults(ctx context.Context) {
// New site: Set default settings.
if s.ID == 0 {
s.Settings.Campaigns = []string{"utm_campaign", "utm_source", "ref"}
s.Settings.EmailReports = EmailReportOnce
}

if s.State == "" {
Expand Down Expand Up @@ -148,6 +165,9 @@ func (s *Site) Validate(ctx context.Context) error {

v.Range("settings.limits.page", int64(s.Settings.Limits.Page), 1, 25)
v.Range("settings.limits.ref", int64(s.Settings.Limits.Ref), 1, 25)
if !intutil.Contains(EmailReports, s.Settings.EmailReports.Int()) {
v.Append("settings.email_reports", "invalid value")
}

if s.Settings.DataRetention > 0 {
v.Range("settings.data_retention", int64(s.Settings.DataRetention), 14, 0)
Expand Down Expand Up @@ -508,6 +528,20 @@ func (s *Sites) List(ctx context.Context) error {
StateActive), "Sites.List")
}

// ListIDs lists all sites with the given IDs.
func (s *Sites) ListIDs(ctx context.Context, ids ...int64) error {
query, args, err := sqlx.In(
`select * from sites where state=? and id in (?) order by created_at desc`,
StateActive, ids)
if err != nil {
return fmt.Errorf("Sites.ListIDs: %w", err)
}

db := zdb.MustGet(ctx)
err = db.SelectContext(ctx, s, db.Rebind(query), args...)
return errors.Wrap(err, "Sites.ListIDs")
}

// ListCnames all sites that have CNAME set.
func (s *Sites) ListCnames(ctx context.Context) error {
return errors.Wrap(zdb.MustGet(ctx).SelectContext(ctx, s,
Expand Down
12 changes: 12 additions & 0 deletions tpl/backend_settings.gohtml
Expand Up @@ -65,6 +65,18 @@
<label for="limits_ref">Referrers page size</label>
<input type="number" min="1" max="25" name="settings.limits.ref" id="limits_ref" value="{{.Site.Settings.Limits.Ref}}">
{{validate "site.settings.limits.ref" .Validate}}

<label for="email_reports">Email reports</label>
<select name="settings.email_reports" id="email_reports">
<option {{option_value .Site.Settings.EmailReports.String "0"}}>Never</option>
<option {{option_value .Site.Settings.EmailReports.String "1"}}>Daily</option>
<option {{option_value .Site.Settings.EmailReports.String "2"}}>Weekly</option>
<option {{option_value .Site.Settings.EmailReports.String "3"}}>Biweekly</option>
<option {{option_value .Site.Settings.EmailReports.String "4"}}>Monthly</option>
</select>

<label for="email_reports_cc">Also Cc to</label>
<input type="text" name="settings.email_reports_cc" id="email_reports_cc" value="{{.Site.Settings.EmailReportsCc}}">
</fieldset>

<fieldset>
Expand Down
1 change: 1 addition & 0 deletions tpl/email_report.gohtml
@@ -0,0 +1 @@
HTML Version
31 changes: 31 additions & 0 deletions tpl/email_report.gotxt
@@ -0,0 +1,31 @@
Hi there!

This is your {{.PeriodName}} GoatCounter report for {{.Site.Code}}

Note: this is the text version of the email and best viewed with a monospaced
typeface.

Path Visitors Pageviews Difference

Totals 13,645 14,143 +5%
{{range $p := .Pages}}
{{$p.Path}} {{$p.CountUnique}} {{$p.PageViews}}
{{end}}
{{/*
Top referrers this week:

reddit.com 1,531
Hacker News 1,531


See https://foo.goatcounter.com for the full details.

if .Once
This email is sent once for new installations; if you wish to keep receiving it
then please enable the setting.
else
This is email is sent because you set it. Please change the setting if you no
longer want to.
end
*/}}
{{template "_email_bottom.gotxt" .}}

0 comments on commit 1b08fcd

Please sign in to comment.