Skip to content

Commit 362720f

Browse files
committed
Gate unique analytics on individual_tracking and harden query prep
- Force unique=false unless individual_tracking is on, so the API never serves distinct-subscriber counts against the privacy setting. - Build the *-unique statements from the unique-count query only when tracking is on; a missing source query now fails with a clear error instead of a nil panic.
1 parent 315a429 commit 362720f

2 files changed

Lines changed: 26 additions & 6 deletions

File tree

cmd/campaigns.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,13 @@ func (a *App) GetCampaignViewAnalytics(c echo.Context) error {
623623
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("analytics.invalidDates"))
624624
}
625625

626+
// Unique (distinct-subscriber) counts are only served when individual
627+
// subscriber tracking is enabled. Otherwise force total counts so the API
628+
// never exposes distinct-subscriber analytics against the privacy setting.
629+
if unique && !a.cfg.Privacy.IndividualTracking {
630+
unique = false
631+
}
632+
626633
// Campaign link stats.
627634
if typ == "links" {
628635
out, err := a.core.GetCampaignAnalyticsLinks(ids, typ, from, to)

cmd/init.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -400,24 +400,37 @@ func readQueries(dir string, fs stuffbin.FileSystem) goyesql.Queries {
400400

401401
// prepareQueries queries prepares a query map and returns a *Queries
402402
func prepareQueries(qMap goyesql.Queries, db *sqlx.DB, ko *koanf.Koanf) *models.Queries {
403+
individualTracking := ko.Bool("privacy.individual_tracking")
404+
405+
// The unique (distinct-subscriber) statements are only used when individual
406+
// tracking is on; when it's off the API never serves unique counts (see
407+
// GetCampaignViewAnalytics), so prepare them from the total query instead of
408+
// depending on the unique-count template being present in a custom query set.
409+
uniqueSrc := "get-campaign-analytics-counts"
410+
if individualTracking {
411+
uniqueSrc = "get-campaign-analytics-unique-counts"
412+
}
413+
403414
// These don't exist in the SQL file but are in the queries struct to be prepared.
404-
// Both total and unique variants are interpolated with table names and prepared,
405-
// so analytics can show total and unique counts side by side when tracking is on.
406415
for _, c := range []struct{ name, src, table string }{
407416
{"get-campaign-view-counts", "get-campaign-analytics-counts", "campaign_views"},
408417
{"get-campaign-click-counts", "get-campaign-analytics-counts", "link_clicks"},
409-
{"get-campaign-view-counts-unique", "get-campaign-analytics-unique-counts", "campaign_views"},
410-
{"get-campaign-click-counts-unique", "get-campaign-analytics-unique-counts", "link_clicks"},
418+
{"get-campaign-view-counts-unique", uniqueSrc, "campaign_views"},
419+
{"get-campaign-click-counts-unique", uniqueSrc, "link_clicks"},
411420
} {
421+
src, ok := qMap[c.src]
422+
if !ok {
423+
lo.Fatalf("SQL query '%s' required for campaign analytics is missing", c.src)
424+
}
412425
qMap[c.name] = &goyesql.Query{
413-
Query: fmt.Sprintf(qMap[c.src].Query, c.table),
426+
Query: fmt.Sprintf(src.Query, c.table),
414427
Tags: map[string]string{"name": c.name},
415428
}
416429
}
417430

418431
// Link counts switch to unique subscribers only when individual tracking is on.
419432
linkSel := "*"
420-
if ko.Bool("privacy.individual_tracking") {
433+
if individualTracking {
421434
linkSel = "DISTINCT subscriber_id"
422435
}
423436
qMap["get-campaign-link-counts"].Query = fmt.Sprintf(qMap["get-campaign-link-counts"].Query, linkSel)

0 commit comments

Comments
 (0)