feat: an attempt to avoid tracking of assets

This commit is contained in:
Oleh Astappiev 2024-10-17 01:59:09 +02:00
parent 93959ce812
commit f0193f763d
No known key found for this signature in database
GPG key ID: 37D7AC22589E9632
3 changed files with 87 additions and 27 deletions

View file

@ -7,8 +7,6 @@ summary: A Traefik plugin that feeds Umami Analytics with pageview events
iconPath: .assets/icon.png iconPath: .assets/icon.png
testData: testData:
disabled: false
debug: false
umamiHost: "" umamiHost: ""
umamiToken: "" umamiToken: ""
umamiUsername: "" umamiUsername: ""
@ -17,5 +15,3 @@ testData:
websites: websites:
domain: websiteId domain: websiteId
createNewWebsites: false createNewWebsites: false
ignoreUserAgents: []
ignoreIPs: []

View file

@ -82,13 +82,15 @@ entryPoints:
## Configuration ## Configuration
| key | default | type | description | | key | default | type | description |
|---------------------|---------|----------|-------------------------------------------------------------------------------------------------------------| |---------------------|---------|------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `umamiHost` | - | `string` | Umami server url, reachable from within traefik (container), e.g. `http://umami:3000` | | `umamiHost` | - | `string` | Umami server url, reachable from within traefik (container), e.g. `http://umami:3000` |
| `umamiToken` | - | `string` | An API Token, used to automatize work with websites, not needed if you provide `websites` | | `umamiToken` | - | `string` | An API Token, used to automatize work with websites, not needed if you provide `websites` |
| `umamiUsername` | - | `string` | An alternative to `umamiToken`, you can provide an username and password | | `umamiUsername` | - | `string` | An alternative to `umamiToken`, you can provide an username and password |
| `umamiPassword` | - | `string` | Only in combination with `umamiUsername` | | `umamiPassword` | - | `string` | Only in combination with `umamiUsername` |
| `umamiTeamId` | - | `string` | In order to organize websites, you can use Umami Teams | | `umamiTeamId` | - | `string` | In order to organize websites, you can use Umami Teams |
| `websites` | - | `map` | A map of hostnames and their associated Umami IDs. Can also be used to override or extend fetched websites | | `websites` | - | `map` | A map of hostnames and their associated Umami IDs. Can also be used to override or extend fetched websites |
| `createNewWebsites` | false | `bool` | If set to `true`, will try to create a new website on Umami, if domain not found there | | `createNewWebsites` | false | `bool` | If set to `true`, will try to create a new website on Umami, if domain not found there |
| `debug` | false | `bool` | Something doesn't work? Set to `true` to see more logs (plugins doesn't have access to Traefik's log level) | | `trackAllResources` | false | `bool` | Defines whether all requests for any resource should be tracked. By default, only requests that are believed to contain content are tracked |
| `trackExtensions` | | `string[]` | Defines an alternative list of file extensions that should be tracked |
| `debug` | false | `bool` | Something doesn't work? Set to `true` to see more logs (plugins doesn't have access to Traefik's log level) |

View file

@ -6,33 +6,45 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path"
"strings" "strings"
"time" "time"
) )
// Config the plugin configuration. // Config the plugin configuration.
type Config struct { type Config struct {
// basic plugin configuration // Disabled disables the plugin.
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
Debug bool `json:"debug"` // Debug enables debug logging, be prepared for flooding.
Debug bool `json:"debug"`
// Umami configuration // UmamiHost is the URL of the Umami instance.
UmamiHost string `json:"umamiHost"` UmamiHost string `json:"umamiHost"`
// it is optional, but either UmamiToken or Websites should be set // UmamiToken is an API KEY, which is optional, but either UmamiToken or Websites should be set.
UmamiToken string `json:"umamiToken"` UmamiToken string `json:"umamiToken"`
// as an alternative to UmamiToken, you can set UmamiUsername and UmamiPassword to authenticate // UmamiUsername could be provided as an alternative to UmamiToken, used to retrieve the token.
UmamiUsername string `json:"umamiUsername"` UmamiUsername string `json:"umamiUsername"`
// UmamiPassword is required if UmamiUsername is set.
UmamiPassword string `json:"umamiPassword"` UmamiPassword string `json:"umamiPassword"`
UmamiTeamId string `json:"umamiTeamId"` // UmamiTeamId defines a team, which will be used to retrieve the websites.
UmamiTeamId string `json:"umamiTeamId"`
// if both UmamiToken and Websites are set, Websites will be used to override the websites in the API // Websites is a map of domain to websiteId, which is required if UmamiToken is not set.
// If both UmamiToken and Websites are set, Websites will override/extend domains retrieved from the API.
Websites map[string]string `json:"websites"` Websites map[string]string `json:"websites"`
// if createNewWebsites is set to true, the plugin will create new websites in the API, UmamiToken is required // CreateNewWebsites when set to true, the plugin will create new websites using API, UmamiToken is required.
CreateNewWebsites bool `json:"createNewWebsites"` CreateNewWebsites bool `json:"createNewWebsites"`
// filters to ignore requests and do not send view events // TrackAllResources defines whether all requests for any resource should be tracked.
// By default, only requests that are believed to contain content are tracked.
TrackAllResources bool `json:"trackAllResources"`
// TrackExtensions defines an alternative list of file extensions that should be tracked.
TrackExtensions []string `json:"trackExtensions"`
// IgnoreUserAgents is a list of user agents that should be ignored.
IgnoreUserAgents []string `json:"ignoreUserAgents"` IgnoreUserAgents []string `json:"ignoreUserAgents"`
IgnoreIPs []string `json:"ignoreIPs"` // IgnoreIPs is a list of IPs that should be ignored.
IgnoreIPs []string `json:"ignoreIPs"`
} }
// CreateConfig creates the default plugin configuration. // CreateConfig creates the default plugin configuration.
@ -50,6 +62,9 @@ func CreateConfig() *Config {
Websites: map[string]string{}, Websites: map[string]string{},
CreateNewWebsites: false, CreateNewWebsites: false,
TrackAllResources: false,
TrackExtensions: []string{},
IgnoreUserAgents: []string{}, IgnoreUserAgents: []string{},
IgnoreIPs: []string{}, IgnoreIPs: []string{},
} }
@ -69,6 +84,9 @@ type UmamiFeeder struct {
websites map[string]string websites map[string]string
createNewWebsites bool createNewWebsites bool
trackAllResources bool
trackExtensions []string
ignoreUserAgents []string ignoreUserAgents []string
ignoreIPs []string ignoreIPs []string
} }
@ -89,6 +107,9 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
websites: config.Websites, websites: config.Websites,
createNewWebsites: config.CreateNewWebsites, createNewWebsites: config.CreateNewWebsites,
trackAllResources: config.TrackAllResources,
trackExtensions: config.TrackExtensions,
ignoreUserAgents: config.IgnoreUserAgents, ignoreUserAgents: config.IgnoreUserAgents,
ignoreIPs: config.IgnoreIPs, ignoreIPs: config.IgnoreIPs,
} }
@ -149,17 +170,17 @@ func (h *UmamiFeeder) verifyConfig(config *Config) error {
func (h *UmamiFeeder) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (h *UmamiFeeder) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !h.isDisabled { if !h.isDisabled {
if h.shouldBeTracked(req) { if h.shouldTrack(req) {
go h.trackRequest(req) go h.trackRequest(req)
} else { } else {
h.debug("Tracking skipped %s", req.URL) h.debug("ignoring request to %s%s", req.Host, req.URL)
} }
} }
h.next.ServeHTTP(rw, req) h.next.ServeHTTP(rw, req)
} }
func (h *UmamiFeeder) shouldBeTracked(req *http.Request) bool { func (h *UmamiFeeder) shouldTrack(req *http.Request) bool {
if len(h.ignoreUserAgents) > 0 { if len(h.ignoreUserAgents) > 0 {
userAgent := req.UserAgent() userAgent := req.UserAgent()
for _, disabledUserAgent := range h.ignoreUserAgents { for _, disabledUserAgent := range h.ignoreUserAgents {
@ -178,6 +199,10 @@ func (h *UmamiFeeder) shouldBeTracked(req *http.Request) bool {
} }
} }
if !h.shouldTrackResource(req.URL.Path) {
return false
}
if h.createNewWebsites { if h.createNewWebsites {
return true return true
} }
@ -190,6 +215,43 @@ func (h *UmamiFeeder) shouldBeTracked(req *http.Request) bool {
return false return false
} }
func (h *UmamiFeeder) shouldTrackResource(url string) bool {
if h.trackAllResources {
return true
}
pathExt := path.Ext(url)
// If a custom file extension list is defined, check if the resource matches it. If not, do not report.
if len(h.trackExtensions) > 0 {
for _, suffix := range h.trackExtensions {
if suffix == pathExt {
return true
}
}
return false
}
// Check if the suffix is regarded to be "content".
switch pathExt {
case ".htm":
case ".html":
case ".xhtml":
case ".jsf":
case ".md":
case ".php":
case ".rss":
case ".rtf":
case ".txt":
case ".xml":
case ".pdf":
case "":
return true
}
return false
}
func (h *UmamiFeeder) trackRequest(req *http.Request) { func (h *UmamiFeeder) trackRequest(req *http.Request) {
hostname := parseDomainFromHost(req.Host) hostname := parseDomainFromHost(req.Host)
websiteId, ok := h.websites[hostname] websiteId, ok := h.websites[hostname]