瀏覽代碼

Make individual subscriber tracking optional.

A new toggle switch in Settings -> Privacy, which is off by
default, allows campaign views (pixel) and link clicks to function
without registering the subscriber ID against view and click
events, anonymising tracking. When off, the subscriber UUIDs in
view and link tracking URLs are removed, anonymising subscriber
information from HTTP logs as well.
Kailash Nadh 4 年之前
父節點
當前提交
1b279478fb
共有 7 個文件被更改,包括 70 次插入35 次删除
  1. 17 15
      cmd/init.go
  2. 10 0
      cmd/public.go
  3. 6 5
      cmd/settings.go
  4. 8 0
      frontend/src/views/Settings.vue
  5. 26 13
      internal/manager/manager.go
  6. 2 2
      queries.sql
  7. 1 0
      schema.sql

+ 17 - 15
cmd/init.go

@@ -45,10 +45,11 @@ type constants struct {
 	FromEmail    string   `koanf:"from_email"`
 	NotifyEmails []string `koanf:"notify_emails"`
 	Privacy      struct {
-		AllowBlocklist bool            `koanf:"allow_blocklist"`
-		AllowExport    bool            `koanf:"allow_export"`
-		AllowWipe      bool            `koanf:"allow_wipe"`
-		Exportable     map[string]bool `koanf:"-"`
+		IndividualTracking bool            `koanf:"individual_tracking"`
+		AllowBlocklist     bool            `koanf:"allow_blocklist"`
+		AllowExport        bool            `koanf:"allow_export"`
+		AllowWipe          bool            `koanf:"allow_wipe"`
+		Exportable         map[string]bool `koanf:"-"`
 	} `koanf:"privacy"`
 	AdminUsername []byte `koanf:"admin_username"`
 	AdminPassword []byte `koanf:"admin_password"`
@@ -263,17 +264,18 @@ func initCampaignManager(q *Queries, cs *constants, app *App) *manager.Manager {
 	}
 
 	return manager.New(manager.Config{
-		BatchSize:     ko.Int("app.batch_size"),
-		Concurrency:   ko.Int("app.concurrency"),
-		MessageRate:   ko.Int("app.message_rate"),
-		MaxSendErrors: ko.Int("app.max_send_errors"),
-		FromEmail:     cs.FromEmail,
-		UnsubURL:      cs.UnsubURL,
-		OptinURL:      cs.OptinURL,
-		LinkTrackURL:  cs.LinkTrackURL,
-		ViewTrackURL:  cs.ViewTrackURL,
-		MessageURL:    cs.MessageURL,
-		UnsubHeader:   ko.Bool("privacy.unsubscribe_header"),
+		BatchSize:          ko.Int("app.batch_size"),
+		Concurrency:        ko.Int("app.concurrency"),
+		MessageRate:        ko.Int("app.message_rate"),
+		MaxSendErrors:      ko.Int("app.max_send_errors"),
+		FromEmail:          cs.FromEmail,
+		IndividualTracking: ko.Bool("privacy.individual_tracking"),
+		UnsubURL:           cs.UnsubURL,
+		OptinURL:           cs.OptinURL,
+		LinkTrackURL:       cs.LinkTrackURL,
+		ViewTrackURL:       cs.ViewTrackURL,
+		MessageURL:         cs.MessageURL,
+		UnsubHeader:        ko.Bool("privacy.unsubscribe_header"),
 	}, newManagerDB(q), campNotifCB, lo)
 
 }

+ 10 - 0
cmd/public.go

@@ -293,6 +293,11 @@ func handleLinkRedirect(c echo.Context) error {
 		subUUID  = c.Param("subUUID")
 	)
 
+	// If individual tracking is disabled, do not record the subscriber ID.
+	if !app.constants.Privacy.IndividualTracking {
+		subUUID = ""
+	}
+
 	var url string
 	if err := app.queries.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil {
 		if err != sql.ErrNoRows {
@@ -318,6 +323,11 @@ func handleRegisterCampaignView(c echo.Context) error {
 		subUUID  = c.Param("subUUID")
 	)
 
+	// If individual tracking is disabled, do not record the subscriber ID.
+	if !app.constants.Privacy.IndividualTracking {
+		subUUID = ""
+	}
+
 	// Exclude dummy hits from template previews.
 	if campUUID != dummyUUID && subUUID != dummyUUID {
 		if _, err := app.queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil {

+ 6 - 5
cmd/settings.go

@@ -25,11 +25,12 @@ type settings struct {
 	AppMaxSendErrors int      `json:"app.max_send_errors"`
 	AppMessageRate   int      `json:"app.message_rate"`
 
-	PrivacyUnsubHeader    bool     `json:"privacy.unsubscribe_header"`
-	PrivacyAllowBlocklist bool     `json:"privacy.allow_blocklist"`
-	PrivacyAllowExport    bool     `json:"privacy.allow_export"`
-	PrivacyAllowWipe      bool     `json:"privacy.allow_wipe"`
-	PrivacyExportable     []string `json:"privacy.exportable"`
+	PrivacyIndividualTracking bool     `json:"privacy.individual_tracking"`
+	PrivacyUnsubHeader        bool     `json:"privacy.unsubscribe_header"`
+	PrivacyAllowBlocklist     bool     `json:"privacy.allow_blocklist"`
+	PrivacyAllowExport        bool     `json:"privacy.allow_export"`
+	PrivacyAllowWipe          bool     `json:"privacy.allow_wipe"`
+	PrivacyExportable         []string `json:"privacy.exportable"`
 
 	UploadProvider             string `json:"upload.provider"`
 	UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`

+ 8 - 0
frontend/src/views/Settings.vue

@@ -104,6 +104,14 @@
 
           <b-tab-item label="Privacy">
             <div class="items">
+              <b-field label="Individual subscriber tracking"
+                message="Track subscriber-level campaign views and clicks.
+                When disabled, view and click tracking continue without
+                being linked to individual subscribers.">
+                <b-switch v-model="form['privacy.individual_tracking']"
+                    name="privacy.individual_tracking" />
+              </b-field>
+
               <b-field label="Include `List-Unsubscribe` header"
                 message="Include unsubscription headers that allow e-mail clients to
                         allow users to unsubscribe in a single click.">

+ 26 - 13
internal/manager/manager.go

@@ -21,6 +21,8 @@ const (
 
 	// ContentTpl is the name of the compiled message.
 	ContentTpl = "content"
+
+	dummyUUID = "00000000-0000-0000-0000-000000000000"
 )
 
 // DataSource represents a data backend, such as a database,
@@ -86,17 +88,18 @@ type Config struct {
 	// Number of subscribers to pull from the DB in a single iteration.
 	BatchSize int
 
-	Concurrency    int
-	MessageRate    int
-	MaxSendErrors  int
-	RequeueOnError bool
-	FromEmail      string
-	LinkTrackURL   string
-	UnsubURL       string
-	OptinURL       string
-	MessageURL     string
-	ViewTrackURL   string
-	UnsubHeader    bool
+	Concurrency        int
+	MessageRate        int
+	MaxSendErrors      int
+	RequeueOnError     bool
+	FromEmail          string
+	IndividualTracking bool
+	LinkTrackURL       string
+	UnsubURL           string
+	OptinURL           string
+	MessageURL         string
+	ViewTrackURL       string
+	UnsubHeader        bool
 }
 
 type msgError struct {
@@ -297,11 +300,21 @@ func (m *Manager) messageWorker() {
 func (m *Manager) TemplateFuncs(c *models.Campaign) template.FuncMap {
 	return template.FuncMap{
 		"TrackLink": func(url string, msg *CampaignMessage) string {
-			return m.trackLink(url, msg.Campaign.UUID, msg.Subscriber.UUID)
+			subUUID := msg.Subscriber.UUID
+			if !m.cfg.IndividualTracking {
+				subUUID = dummyUUID
+			}
+
+			return m.trackLink(url, msg.Campaign.UUID, subUUID)
 		},
 		"TrackView": func(msg *CampaignMessage) template.HTML {
+			subUUID := msg.Subscriber.UUID
+			if !m.cfg.IndividualTracking {
+				subUUID = dummyUUID
+			}
+
 			return template.HTML(fmt.Sprintf(`<img src="%s" alt="" />`,
-				fmt.Sprintf(m.cfg.ViewTrackURL, msg.Campaign.UUID, msg.Subscriber.UUID)))
+				fmt.Sprintf(m.cfg.ViewTrackURL, msg.Campaign.UUID, subUUID)))
 		},
 		"UnsubscribeURL": func(msg *CampaignMessage) string {
 			return msg.unsubURL

+ 2 - 2
queries.sql

@@ -591,7 +591,7 @@ DELETE FROM campaigns WHERE id=$1;
 -- name: register-campaign-view
 WITH view AS (
     SELECT campaigns.id as campaign_id, subscribers.id AS subscriber_id FROM campaigns
-    LEFT JOIN subscribers ON (subscribers.uuid = $2)
+    LEFT JOIN subscribers ON (CASE WHEN $2::TEXT != '' THEN subscribers.uuid = $2::UUID ELSE FALSE END)
     WHERE campaigns.uuid = $1
 )
 INSERT INTO campaign_views (campaign_id, subscriber_id)
@@ -674,7 +674,7 @@ INSERT INTO links (uuid, url) VALUES($1, $2) ON CONFLICT (url) DO UPDATE SET url
 WITH link AS (
     SELECT url, links.id AS link_id, campaigns.id as campaign_id, subscribers.id AS subscriber_id FROM links
     LEFT JOIN campaigns ON (campaigns.uuid = $2)
-    LEFT JOIN subscribers ON (subscribers.uuid = $3)
+    LEFT JOIN subscribers ON (CASE WHEN $3::TEXT != '' THEN subscribers.uuid = $3::UUID ELSE FALSE END)
     WHERE links.uuid = $1
 )
 INSERT INTO link_clicks (campaign_id, subscriber_id, link_id)

+ 1 - 0
schema.sql

@@ -174,6 +174,7 @@ INSERT INTO settings (key, value) VALUES
     ('app.batch_size', '1000'),
     ('app.max_send_errors', '1000'),
     ('app.notify_emails', '["admin1@mysite.com", "admin2@mysite.com"]'),
+    ('privacy.individual_tracking', 'false'),
     ('privacy.unsubscribe_header', 'true'),
     ('privacy.allow_blocklist', 'true'),
     ('privacy.allow_export', 'true'),