Skip to content

Commit

Permalink
Better documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaharagon committed Apr 16, 2024
1 parent 47fdd0f commit 0364534
Showing 1 changed file with 97 additions and 12 deletions.
109 changes: 97 additions & 12 deletions umami.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,111 @@ func init() {
httpcaddyfile.RegisterHandlerDirective("umami", parseCaddyfile)
}

// A Caddy module which sends visitor information to Umami's Events REST API endpoint.
// http.handlers.umami is a module for Caddy which sends HTTP request information to [Umami]'s Events REST API endpoint as page view events.
// The data is sent directly from the web server to Umami's [/api/send endpoint] without requiring you to include any client-side JavaScript on your website.
// The visitor's IP is set via the X-Forwarded-For header or the header specified with client_ip_header,
// one of which should be [set as CLIENT_IP_HEADER in Umami].
//
// # Minimal example (Caddyfile)
//
// umami {
// event_endpoint https://umami.example.com/api/send
// website_uuid your-website-uuid
// }
//
// # Complete example (Caddyfile)
//
// umami {
// event_endpoint https://umami.example.com/api/send
// website_uuid your-website-uuid
// allowed_extensions "" .htm .html .php
// client_ip_header X-Real-IP
// report_all_resources
// trusted_ip_header X-Forwarded-For
// cookie_resolution umami_resolution
// device_detection
// cookie_consent umami_consent disable_all
// static_metadata {
// server node1
// }
// }
//
// [/api/send endpoint]: https://umami.is/docs/sending-stats
// [set as CLIENT_IP_HEADER in Umami]: https://umami.is/docs/environment-variables
// [Umami]: https://umami.is
type Umami struct {
// The address of your Umami installation's send API endpoint.
// The address of your Umami installation's send API endpoint, usually ending in /api/send.
//
// # Example (Caddyfile)
// event_endpoint https://umami.example.com/api/send
EventEndpoint string `json:"event_endpoint"`
// The UUID of the website you want to track.
// The UUID of the website you are tracking, from the Umami control panel.
//
// # Example (Caddyfile)
// website_uuid 0cdf6f5b-9b3b-4815-9d22-8cb4d6132781
WebsiteUUID string `json:"website_uuid"`
// A map of page path extensions that should be reported to Umami. You must include the leading dot.
// Include "" if you want to track URLs which don't end in a file extension, such as directories.
//
// # Defaults
// - ""
// - ".htm"
// - ".html"
// - ".php"
// # Example (Caddyfile)
// allowed_extensions "" .html
AllowedExtensions []string `json:"allowed_extensions,omitempty"`
// The header to send the client IP address to Umami with.
// An HTTP header to send the client IP address to Umami with, alongside the X-Forwarded-For header (which is always sent).
//
// # Example (Caddyfile)
// client_ip_header X-Real-IP
ClientIPHeader string `json:"client_ip_header,omitempty"`
// Enables reporting of all resources (ignoring extension checks).
// Enables reporting of all resources (ignoring extension checks). Overrides allowed_extensions.
//
// # Example (Caddyfile)
// report_all_resources
ReportAllResources bool `json:"report_all_resources,omitempty"`
// The header to use to get the client IP address from, behind a trusted reverse proxy.
// The IP in this header will be sent to Umami via the X-Forwarded-For header and the Client IP Header (if specified).
//
// # Example (Caddyfile)
// trusted_ip_header X-Forwarded-For
TrustedIPHeader string `json:"trusted_ip_header,omitempty"`
// A map of cookie-based consent settings. Only the first value in the map is utilized currently.
CookieConsent []CookieConsent `json:"cookie_consent,omitempty"`
// The name of the cookie that stores the visitor's screen resolution.
// It is your responsibility to set this cookie with client-side JavaScript (not provided).
// If this cookie is not set, device type will just be reported as unknown.
// If included and a name is not set, the default cookie name is umami_resolution.
//
// # Example (Caddyfile)
// cookie_resolution umami_resolution
CookieResolution string `json:"cookie_resolution,omitempty"`
// Enable rudimentary device detection based on Sec-CH-UA-Mobile and Sec-CH-UA-Platform headers.
// Note that this is not a replacement for a proper device detection library.
// A screen resolution set via a cookie (configured separately) will always take precedence over this.
//
// # Example (Caddyfile)
// device_detection
DeviceDetection bool `json:"device_detection,omitempty"`
// A map of cookie-based consent settings. Only the first value in the map is utilized currently.
// Specify the name of a cookie, then:
// You can set the behavior to "disable_all" to disable sending analytics if the cookie value is "false",
// or "path_only" to send analytic data without client information (IP, user agent, etc.) if the cookie value is "false".
//
// # Example (Caddyfile)
// cookie_consent umami_consent disable_all
CookieConsent []CookieConsent `json:"cookie_consent,omitempty"`
// Optional static metadata to include with each event via query string.
// You can include multiple key value pairs, each will be appended as a separate query string.
//
// # Example (Caddyfile)
// static_metadata {
// server node1
// }
StaticMetadata []StaticMetadata `json:"static_metadata,omitempty"`

logger *zap.Logger
}

// Cookie-based consent settings.
type CookieConsent struct {
// The name of the cookie that stores the consent setting.
Name string `json:"name,omitempty"`
Expand All @@ -58,7 +136,6 @@ type CookieConsent struct {
Behavior string `json:"behavior,omitempty"`
}

// Optional static metadata to include with each event via query string.
type StaticMetadata struct {
// The key of the metadata.
Key string `json:"key,omitempty"`
Expand All @@ -84,6 +161,7 @@ func (p Umami) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.
return err
}

// Check if analytics should be performed based on cookie consent settings.
p.logger.Debug("Check if analytics should be performed:", zap.Int("allowed", p.GetAllowed(r)))
if p.GetAllowed(r) == 0 {
return nil
Expand Down Expand Up @@ -187,6 +265,7 @@ func (p Umami) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.
req.Header.Set("User-Agent", "Mozilla/5.0 (Unknown) Browser/1.0 (Anonymous Request)")
}

// Set client IP address in headers.
req.Header.Set("X-Forwarded-For", visitorIP)
if p.ClientIPHeader != "" {
req.Header.Set(p.ClientIPHeader, visitorIP)
Expand All @@ -196,6 +275,7 @@ func (p Umami) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.
p.logger.Debug("User-Agent:", zap.String("User-Agent", req.UserAgent()))
p.logger.Debug("Body:", zap.Any("visitorInfo", visitorInfo))

// Send visitor info to Umami API.
resp, err := client.Do(req)
if err != nil {
p.logger.Warn("Error sending visitor info", zap.Error(err))
Expand All @@ -205,6 +285,7 @@ func (p Umami) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.
}
defer resp.Body.Close()

// Log error response from Umami API.
if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body)
if err != nil {
Expand All @@ -220,10 +301,11 @@ func (p Umami) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.
}

// Check whether analytics should be performed based on cookie consent settings.
// 0 - no analytics
// 1 - all analytics
// 2 - only path analytics
// - 0 - no analytics
// - 1 - all analytics
// - 2 - only path analytics
func (p *Umami) GetAllowed(r *http.Request) int {
// Only the first value in the map is utilized currently.
if len(p.CookieConsent) != 0 {
if p.CookieConsent[0].Behavior == "path_only" {
cookie, err := r.Cookie(p.CookieConsent[0].Name)
Expand Down Expand Up @@ -393,6 +475,7 @@ func (p *Umami) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}

// Read static metadata from Caddyfile either as a single key value pair or as a block.
func ParseCaddyfileStaticMetadata(d *caddyfile.Dispenser) ([]StaticMetadata, error) {
var metadata []StaticMetadata

Expand Down Expand Up @@ -437,6 +520,8 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
return umami, nil
}

// Validate the Umami middleware configuration.
// Validate implements caddy.Validator.
func (p *Umami) Validate() error {
if p.EventEndpoint == "" {
return fmt.Errorf("no event endpoint provided")
Expand Down

0 comments on commit 0364534

Please sign in to comment.