diff --git a/.assets/icon.png b/.assets/icon.png index cd5f556..ceacf8e 100644 Binary files a/.assets/icon.png and b/.assets/icon.png differ diff --git a/.traefik.yml b/.traefik.yml index c53497a..e92c6ec 100644 --- a/.traefik.yml +++ b/.traefik.yml @@ -1,17 +1,13 @@ -displayName: Traefik Umami Feeder +displayName: Traefik Rybbit Feeder -import: github.com/astappiev/traefik-umami-feeder +import: github.com/foxxmd/traefik-rybbit-feeder type: middleware -summary: A Traefik plugin that feeds Umami Analytics with pageview events +summary: A Traefik plugin that feeds Rybbit Analytics with pageview events iconPath: .assets/icon.png testData: - umamiHost: "" - umamiToken: "" - umamiUsername: "" - umamiPassword: "" - umamiTeamId: "" + Host: "" + ApiKey: "" websites: - domain: websiteId - createNewWebsites: false + localhost: siteId diff --git a/demo/config.yml b/demo/config.yml index 1229fea..fba213f 100644 --- a/demo/config.yml +++ b/demo/config.yml @@ -1,11 +1,11 @@ http: middlewares: - umami: + rybbit: plugin: - umami-feeder: + rybbit-feeder: debug: true - umamiHost: http://umami:3000 - umamiUsername: "admin" - umamiPassword: "umami" - umamiTeamId: "8e39c6ad-e44a-4d3e-be98-015db2d62d40" - createNewWebsites: true + host: https://rybbit.mysite.com + apiKey: rb_a0938f333a88239c6250c2c3707a + websites: + "localhost": "1" + batchSize: 1 diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index aadf351..0e81268 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -1,8 +1,6 @@ services: traefik: image: traefik:3 - depends_on: - - umami labels: - "traefik.enable=true" - "traefik.http.routers.traefik.service=api@internal" @@ -24,7 +22,7 @@ services: - ./traefik.yml:/traefik.yml:ro - ./config.yml:/config/config.yml:ro # mount plugin - - ../:/plugins-local/src/github.com/astappiev/traefik-umami-feeder + - ../:/plugins-local/src/github.com/foxxmd/traefik-rybbit-feeder restart: unless-stopped @@ -32,7 +30,7 @@ services: image: containous/whoami ports: - target: 80 - published: 80 + published: 7654 protocol: tcp labels: - "traefik.enable=true" @@ -41,44 +39,10 @@ services: - "traefik.http.routers.whoami.rule=Host(`localhost`)" - "traefik.http.routers.whoami.entrypoints=whoami" # middleware - - "traefik.http.routers.whoami.middlewares=umami@file" + - "traefik.http.routers.whoami.middlewares=rybbit@file" networks: - traefik - - umami: - image: ghcr.io/umami-software/umami:postgresql-latest - depends_on: - postgres: - condition: service_healthy - environment: - - DEBUG=umami:middleware - - ENABLE_TEST_CONSOLE=1 - - DATABASE_URL=postgres://umami:umami@postgres:5432/umami - ports: - - target: 3000 - published: 3000 - protocol: tcp - networks: - - traefik - - umami_postgres - - postgres: - image: postgres:16 - environment: - - POSTGRES_PASSWORD=umami - - POSTGRES_DB=umami - - POSTGRES_USER=umami - networks: - - umami_postgres - volumes: - - ./postgres:/var/lib/postgresql/data - healthcheck: - test: [ "CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'" ] - interval: 10s - timeout: 3s - retries: 3 - networks: traefik: - umami_postgres: + name: traefik diff --git a/demo/traefik.yml b/demo/traefik.yml index dafaeec..6d8727e 100644 --- a/demo/traefik.yml +++ b/demo/traefik.yml @@ -18,13 +18,11 @@ entryPoints: address: ":8080" "whoami": # http address: ":8081" - "umami": # umami dashboard - address: ":3000" api: dashboard: true experimental: localPlugins: - umami-feeder: - moduleName: "github.com/astappiev/traefik-umami-feeder" + rybbit-feeder: + moduleName: "github.com/foxxmd/traefik-rybbit-feeder" diff --git a/go.mod b/go.mod index 726d182..535f40f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/astappiev/traefik-umami-feeder +module github.com/FoxxMD/traefik-rybbit-feeder go 1.21 diff --git a/readme.md b/readme.md index 0c160b3..33ffe76 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,13 @@ -# Traefik Umami Feeder Plugin +# Traefik Rybbit Feeder Plugin -A [Traefik](https://traefik.io/traefik/) middleware plugin that sends visits to your [Umami](https://umami.is) instance. +A [Traefik](https://traefik.io/traefik/) middleware plugin that sends visits to your [Rybbit](https://www.rybbit.io/) instance. -It was created as an alternative to [traefik-umami-plugin](https://github.com/1cedsoda/traefik-umami-plugin) and -inspired by the [Plausible Feeder Traefik Plugin](https://github.com/safing/plausiblefeeder). +**This is a fork of [traefik-umami-feeder](https://github.com/astappiev/traefik-umami-feeder) modified to work with Rybbit.** ## Introduction -This plugin integrates your Traefik-proxied services with Umami, a simple, fast, privacy-focused analytics solution. It -captures basic request information (path, user-agent, referrer, screen size, IP) and forwards it to your Umami instance, +This plugin integrates your Traefik-proxied services with Rybbit, a simple, fast, privacy-focused analytics solution. It +captures basic request information (path, user-agent, referrer, IP) and forwards it to your Rybbit instance, enabling server-side analytics. Key features: @@ -26,51 +25,42 @@ Declare the plugin in your Traefik **static configuration**. ```yaml experimental: plugins: - umami-feeder: - moduleName: github.com/astappiev/traefik-umami-feeder - version: v1.3.0 # Replace with the latest version + rybbit-feeder: + moduleName: github.com/foxxmd/traefik-rybbit-feeder + version: v0.13.0 # Replace with the latest version ``` ### Step 2. Configure the middleware Once the plugin is declared, configure it as a middleware in your Traefik **dynamic configuration**. -You can specify which websites to track in two ways: +You will need to specify at least three pieces of data: -1. **Manual**: Directly provide a `websites` map, associating hostnames with their Umami Website IDs. -2. **Automatic**: Configure the plugin with your Umami API `umamiToken`, or `umamiUsername` and `umamiPassword`. The - plugin will then automatically fetch the list of websites and their IDs from your Umami instance. - * Optionally, use `umamiTeamId` to scope website retrieval to a specific team. - * Optionally, enable `createNewWebsites` to allow the plugin to create new website entries in Umami if they don't - already exist. +* `host` - Your Rybbit instance base URL. This is URL **without** `/api/track`. +* `apiKey` - The [**Api Key**](https://www.rybbit.io/docs/api#steps) generated for the website you wish to track. +* `websites` - A map of `domain` - `site-id` properties to tell the feeder what traffic should go to your Rybbit Site -See the [Middleware Options](#middleware-options) section for detailed configuration options. +There are additional, optional [Middleware Options](#middleware-options) to configure more behavior below. ```yaml http: middlewares: - my-umami-middleware: + my-rybbit-middleware: plugin: - umami-feeder: - umamiHost: "http://umami:3000" # URL of your Umami instance + rybbit-feeder: + host: "http://rybbit.mysite.com" # URL of your Rybbit instance - # Option 1: Define the list of websites - # websites: - # "example.com": "d4617504-241c-4797-8eab-5939b367b3ad" + apiKey: rb_a0938250c2c2efd061c8250c2c3707a - # Option 2: Use Umami credentials to fetch websites - umamiUsername: "your-umami-username" - umamiPassword: "your-umami-password" - # umamiToken: "your-umami-api-token" # Alternative to username/password - - # Optional: allow creation of new websites in Umami - createNewWebsites: true + websites: + # domain to capture traffic for and the site-id from Rybbit + "example.com": "1" ``` ### Step 3. Attach the middleware to your routers Apply the [configured middleware](https://doc.traefik.io/traefik/routing/routers/#middlewares_1) to the Traefik routers -you want to track with Umami. This is also done in your **dynamic configuration**. +you want to track with Rybbit. This is also done in your **dynamic configuration**. Remember to use the correct [provider namespace](https://doc.traefik.io/traefik/providers/overview/#provider-namespace) (e.g., `@file` if @@ -79,7 +69,7 @@ your middleware is defined in a file, `@docker` if defined via Docker labels). **Example using Docker labels:** ```yaml -- "traefik.http.routers.whoami.middlewares=my-umami-middleware@file" +- "traefik.http.routers.whoami.middlewares=my-rybbit-middleware@file" ``` **Example using a dynamic configuration file (e.g., `dynamic_conf.yml`):** @@ -90,7 +80,7 @@ http: whoami: rule: "Host(`example.com`)" middlewares: - - my-umami-middleware@file + - my-rybbit-middleware@file ``` **Example using static configuration (e.g., `traefik.yml`), by attaching the middleware to an entryPoint to apply it @@ -101,30 +91,25 @@ entryPoints: web: http: middlewares: - - my-umami-middleware@file + - my-rybbit-middleware@file ``` ## Middleware Options -| key | default | type | description | -|---------------------|-----------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `disabled` | `false` | `bool` | Set to `true` to disable the plugin. | -| `debug` | `false` | `bool` | Set to `true` for verbose logging. Useful for troubleshooting as plugins don't inherit Traefik's global log level. | -| `queueSize` | `1000` | `int` | Maximum number of tracking events to queue before sending to the Umami server. | -| `umamiHost` | **required** | `string` | URL of your Umami instance, reachable from Traefik (e.g., `http://umami:3000`). | -| `umamiToken` | - | `string` | [Umami API Token](https://umami.is/docs/api/authentication) for authenticating with your Umami instance. Use this *or* `umamiUsername`/`umamiPassword`. Required for automatic website fetching or creation. | -| `umamiUsername` | - | `string` | Username for Umami authentication. Use this with `umamiPassword` if not using `umamiToken`. Required for automatic website fetching or creation. | -| `umamiPassword` | - | `string` | Password for Umami authentication, used in conjunction with `umamiUsername`. | -| `umamiTeamId` | - | `string` | Optional. If using automatic mode, specifies the Umami Team ID to scope website fetching/creation. | -| `websites` | - | `map` | A map of `hostname: umamiWebsiteID`. Used for manual website configuration or to override/extend websites fetched in automatic mode. | -| `createNewWebsites` | `false` | `bool` | If `true` and using automatic mode, the plugin will attempt to create a new website entry in Umami if the domain is not found. | -| `trackErrors` | `false` | `bool` | If `true`, tracks errors (status codes >= 400). | -| `trackAllResources` | `false` | `bool` | If `true`, tracks requests for all resources. By default, only requests likely to be page views (e.g., HTML, or no specific extension) are tracked. | -| `trackExtensions` | `[see sources]` | `string[]` | A list of specific file extensions to track (e.g., `[".html", ".php"]`). | -| `ignoreUserAgents` | `[]` | `string[]` | A list of user-agent substrings. Requests with matching user-agents will be ignored (e.g., `["Googlebot", "Uptime-Kuma"]`). Matched with `strings.Contains`. | -| `ignoreURLs` | `[]` | `string[]` | A list of regular expressions. Requests with URLs matching any of these patterns will be ignored (e.g., `["/health", "https?://[^/]+/health$"]`). Matched with `regexp.Compile.MatchString`. | -| `ignoreIPs` | `[]` | `string[]` | A list of IP addresses or CIDR ranges to ignore (e.g., `["127.0.0.1", "10.0.0.1/16"]`). Matched with `netip.ParsePrefix.Contains`. | -| `headerIp` | `X-Real-Ip` | `string` | The HTTP header to inspect for the client's real IP address, typically used when Traefik is behind another proxy. | +| key | default | type | description | +| ------------------- | :-------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `false` | `bool` | Set to `true` to disable the plugin. | +| `debug` | `false` | `bool` | Set to `true` for verbose logging. Useful for troubleshooting as plugins don't inherit Traefik's global log level. | +| `host` | **required** | `string` | URL of your Rybbit instance, reachable from Traefik (e.g., `https://rybbit.mydomain.com`). | +| `apiKey` | **required** | `string` | [Rybbit API Key](https://www.rybbit.io/docs/api#steps) for authenticating with your Rybbit instance. | +| `websites` | **required** | `map` | A map of `hostname: site-id` | +| `trackErrors` | `false` | `bool` | If `true`, tracks errors (status codes >= 400). | +| `trackAllResources` | `false` | `bool` | If `true`, tracks requests for all resources. By default, only requests likely to be page views (e.g., HTML, or no specific extension) are tracked. | +| `trackExtensions` | `[see sources]` | `string[]` | A list of specific file extensions to track (e.g., `[".html", ".php"]`). | +| `ignoreUserAgents` | `[]` | `string[]` | A list of user-agent substrings. Requests with matching user-agents will be ignored (e.g., `["Googlebot", "Uptime-Kuma"]`). Matched with `strings.Contains`. | +| `ignoreURLs` | `[]` | `string[]` | A list of regular expressions. Requests with URLs matching any of these patterns will be ignored (e.g., `["/health", "https?://[^/]+/health$"]`). Matched with `regexp.Compile.MatchString`. | +| `ignoreIPs` | `[]` | `string[]` | A list of IP addresses or CIDR ranges to ignore (e.g., `["127.0.0.1", "10.0.0.1/16"]`). Matched with `netip.ParsePrefix.Contains`. | +| `headerIp` | `X-Real-Ip` | `string` | The HTTP header to inspect for the client's real IP address, typically used when Traefik is behind another proxy. | ## Contributing diff --git a/responsewriter.go b/responsewriter.go index 82505db..745829f 100644 --- a/responsewriter.go +++ b/responsewriter.go @@ -1,4 +1,4 @@ -package traefik_umami_feeder +package traefik_rybbit_feeder import ( "bufio" diff --git a/umami.go b/umami.go index 8800374..df4647e 100644 --- a/umami.go +++ b/umami.go @@ -1,4 +1,4 @@ -package traefik_umami_feeder +package traefik_rybbit_feeder import ( "context" @@ -21,29 +21,20 @@ type Config struct { Disabled bool `json:"disabled"` // Debug enables debug logging, be prepared for flooding. Debug bool `json:"debug"` - // QueueSize defines the size of queue, i.e. the amount of events that are waiting to be submitted to Umami. + // QueueSize defines the size of queue, i.e. the amount of events that are waiting to be submitted to Rybbit. QueueSize int `json:"queueSize"` - // BatchSize defines the amount of events that are submitted to Umami in one request. + // BatchSize defines the amount of events that are submitted to Rybbit in one request, should always be 1. BatchSize int `json:"batchSize"` - // BatchMaxWait defines the maximum time to wait before submitting the batch. + // BatchMaxWait defines the maximum time to wait before submitting the batch. Should be 1 second. BatchMaxWait time.Duration `json:"batchMaxWait"` - // UmamiHost is the URL of the Umami instance. - UmamiHost string `json:"umamiHost"` - // UmamiToken is an API KEY, which is optional, but either UmamiToken or Websites should be set. - UmamiToken string `json:"umamiToken"` - // UmamiUsername could be provided as an alternative to UmamiToken, used to retrieve the token. - UmamiUsername string `json:"umamiUsername"` - // UmamiPassword is required if UmamiUsername is set. - UmamiPassword string `json:"umamiPassword"` - // UmamiTeamId defines a team, which will be used to retrieve the websites. - UmamiTeamId string `json:"umamiTeamId"` + // Host is the URL of the Rybbit instance. + Host string `json:"host"` + // APIKey is the API Key generated in Site Settings for a Rybbit Website + APIKey string `json:"apiKey"` - // 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 is a map of domain to site-id, which is required Websites map[string]string `json:"websites"` - // CreateNewWebsites when set to true, the plugin will create new websites using API, UmamiToken is required. - CreateNewWebsites bool `json:"createNewWebsites"` // TrackErrors defines whether errors (status codes >= 400) should be tracked. TrackErrors bool `json:"trackErrors"` @@ -73,14 +64,10 @@ func CreateConfig() *Config { BatchMaxWait: 5 * time.Second, TrackErrors: false, - UmamiHost: "", - UmamiToken: "", - UmamiUsername: "", - UmamiPassword: "", - UmamiTeamId: "", + Host: "", + APIKey: "", - Websites: map[string]string{}, - CreateNewWebsites: false, + Websites: map[string]string{}, TrackAllResources: false, TrackExtensions: []string{}, @@ -99,14 +86,13 @@ type UmamiFeeder struct { isDebug bool isDisabled bool logHandler *log.Logger - queue chan *UmamiEvent + queue chan *RybbitEvent batchSize int batchMaxWait time.Duration - umamiHost string - umamiToken string - umamiTeamId string + host string + apiKey string websites map[string]string websitesMutex sync.RWMutex createNewWebsites bool @@ -131,16 +117,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h isDisabled: config.Disabled, logHandler: log.New(os.Stdout, "", 0), - queue: make(chan *UmamiEvent, config.QueueSize), + queue: make(chan *RybbitEvent, config.QueueSize), batchSize: config.BatchSize, - batchMaxWait: config.BatchMaxWait, + batchMaxWait: 1 * time.Second, - umamiHost: config.UmamiHost, - umamiToken: config.UmamiToken, - umamiTeamId: config.UmamiTeamId, - websites: config.Websites, - websitesMutex: sync.RWMutex{}, - createNewWebsites: config.CreateNewWebsites, + host: config.Host, + apiKey: config.APIKey, + websites: config.Websites, + websitesMutex: sync.RWMutex{}, trackErrors: config.TrackErrors, trackAllResources: config.TrackAllResources, @@ -154,6 +138,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h if !h.isDisabled { h.isDisabled = true + h.debug("batchSize %d", h.batchSize) + h.debug("batchMaxWait %v", h.batchMaxWait) go h.retryConnection(ctx, config) } @@ -178,11 +164,11 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) { select { case <-time.After(currentDelay): retryAttempt++ - h.debug("Attempting to connect to Umami (attempt #%d)...", retryAttempt) + h.debug("Attempting to connect to Rybbit (attempt #%d)...", retryAttempt) err := h.connect(ctx, config) if err == nil { - h.debug("Successfully connected to Umami. Verifying configuration...") + h.debug("Successfully connected to Rybbit. Verifying configuration...") err = h.verifyConfig(config) if err == nil { @@ -197,7 +183,7 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) { return // Exit retry goroutine, plugin remains disabled. } - h.error("Failed to reconnect to Umami: " + err.Error()) + h.error("Failed to reconnect to Rybbit: " + err.Error()) case <-ctx.Done(): h.debug("Context cancelled during retryConnection, stopping connection retries.") return @@ -206,42 +192,21 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) { } func (h *UmamiFeeder) connect(ctx context.Context, config *Config) error { - if h.umamiHost == "" { - return fmt.Errorf("`umamiHost` is not set") + if h.host == "" { + return fmt.Errorf("`host` is not set") } - if config.UmamiUsername != "" && config.UmamiPassword != "" { - token, err := getToken(ctx, h.umamiHost, config.UmamiUsername, config.UmamiPassword) - if err != nil { - return fmt.Errorf("failed to get token: %w", err) - } - if token == "" { - return fmt.Errorf("retrieved token is empty") - } - h.debug("token received %s", token) - h.umamiToken = token - } - if h.umamiToken == "" && len(h.websites) == 0 { - return fmt.Errorf("either `umamiToken` or `websites` should be set") - } - if h.umamiToken == "" && h.createNewWebsites { - return fmt.Errorf("`umamiToken` is required to create new websites") + if h.apiKey == "" { + return fmt.Errorf("`apiKey` should be set") } - if h.umamiToken != "" { - websites, err := fetchWebsites(ctx, h.umamiHost, h.umamiToken, h.umamiTeamId) - if err != nil { - return fmt.Errorf("failed to fetch websites: %w", err) - } + if len(h.websites) == 0 { + return fmt.Errorf("`websites` should not be empty") + } - for _, website := range *websites { - if _, ok := h.websites[website.Domain]; ok { - continue - } - - h.websites[website.Domain] = website.ID - } - h.debug("websites fetched: %v", h.websites) + _, err := sendRequest(ctx, h.host+"/health", nil, nil) + if err != nil { + return fmt.Errorf("Failed to get health for rybbit: %w", err) } return nil diff --git a/umami_test.go b/umami_test.go index c3408f5..8cd3891 100644 --- a/umami_test.go +++ b/umami_test.go @@ -1,4 +1,4 @@ -package traefik_umami_feeder +package traefik_rybbit_feeder import ( "context" @@ -9,11 +9,11 @@ import ( func TestTraefikUmamiFeeder(t *testing.T) { cfg := CreateConfig() - cfg.UmamiHost = "http://localhost:3000" - cfg.UmamiUsername = "admin" - cfg.UmamiPassword = "umami" - cfg.UmamiTeamId = "8e39c6ad-e44a-4d3e-be98-015db2d62d40" - cfg.CreateNewWebsites = true + cfg.Host = "http://localhost:3000" + cfg.APIKey = "8e39c6ad-e44a-4d3e-be98-015db2d62d40" + cfg.Websites = map[string]string{ + "localhost": "4", + } ctx := context.Background() next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) diff --git a/umami_token.go b/umami_token.go deleted file mode 100644 index 6d1ce19..0000000 --- a/umami_token.go +++ /dev/null @@ -1,26 +0,0 @@ -package traefik_umami_feeder - -import "context" - -type authRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type authResponse struct { - Token string `json:"token"` -} - -func getToken(ctx context.Context, umamiHost string, umamiUsername string, umamiPassword string) (string, error) { - var result authResponse - err := sendRequestAndParse(ctx, umamiHost+"/api/auth/login", authRequest{ - Username: umamiUsername, - Password: umamiPassword, - }, nil, &result) - - if err != nil { - return "", err - } - - return result.Token, nil -} diff --git a/umami_utils.go b/umami_utils.go index c44fbed..ead0f1f 100644 --- a/umami_utils.go +++ b/umami_utils.go @@ -1,4 +1,4 @@ -package traefik_umami_feeder +package traefik_rybbit_feeder import ( "bytes" diff --git a/umami_websites.go b/umami_websites.go deleted file mode 100644 index bf61218..0000000 --- a/umami_websites.go +++ /dev/null @@ -1,87 +0,0 @@ -package traefik_umami_feeder - -import ( - "context" - "net/http" - "time" -) - -type websitesResponse struct { - Data []Website `json:"data"` - Count int `json:"count"` - Page int `json:"page"` - PageSize int `json:"pageSize"` - OrderBy string `json:"orderBy"` -} - -type Website struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - TeamId string `json:"teamId,omitempty"` - Domain string `json:"domain,omitempty"` - CreatedAt time.Time `json:"createdAt,omitempty"` -} - -func createWebsite(ctx context.Context, umamiHost string, umamiToken string, teamId string, websiteDomain string) (*Website, error) { - var headers = make(http.Header) - headers.Set("Authorization", "Bearer "+umamiToken) - - var result Website - err := sendRequestAndParse(ctx, umamiHost+"/api/websites", Website{ - Name: websiteDomain, - Domain: websiteDomain, - TeamId: teamId, - }, headers, &result) - - if err != nil { - return nil, err - } - - return &result, nil -} - -func fetchWebsites(ctx context.Context, umamiHost string, umamiToken string, teamId string) (*[]Website, error) { - var headers = make(http.Header) - headers.Set("Authorization", "Bearer "+umamiToken) - - url := umamiHost + "/api/websites?pageSize=200" - if len(teamId) != 0 { - url = umamiHost + "/api/teams/" + teamId + "/websites?pageSize=200" - } - - var result websitesResponse - err := sendRequestAndParse(ctx, url, nil, headers, &result) - - if err != nil { - return nil, err - } - - return &result.Data, nil -} - -func getWebsiteId(h *UmamiFeeder, hostname string) string { - h.websitesMutex.RLock() - websiteId, ok := h.websites[hostname] - h.websitesMutex.RUnlock() - - if ok { - return websiteId - } - - h.websitesMutex.Lock() - defer h.websitesMutex.Unlock() - - // Create a background context for the API call - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - website, err := createWebsite(ctx, h.umamiHost, h.umamiToken, h.umamiTeamId, hostname) - if err != nil { - h.error("failed to create website: " + err.Error()) - return "" - } - - h.websites[website.Domain] = website.ID - h.debug("website created '%s': %s", website.Domain, website.ID) - return website.ID -} diff --git a/umami_worker.go b/umami_worker.go index 44ec173..8a97dbd 100644 --- a/umami_worker.go +++ b/umami_worker.go @@ -1,4 +1,4 @@ -package traefik_umami_feeder +package traefik_rybbit_feeder import ( "context" @@ -8,48 +8,48 @@ import ( "time" ) -type UmamiEvent struct { - Website string `json:"website"` // Website ID - Hostname string `json:"hostname"` // Name of host - Language string `json:"language,omitempty"` // Language of visitor (ex. "en-US") - Referrer string `json:"referrer,omitempty"` // Referrer URL - Url string `json:"url"` // Page URL - Ip string `json:"ip,omitempty"` // IP address - UserAgent string `json:"userAgent,omitempty"` // User agent - Timestamp int64 `json:"timestamp,omitempty"` // UNIX timestamp in seconds - //Data map[string]interface{} `json:"data,omitempty"` // Additional data for the event - //Name string `json:"name,omitempty"` // Event name (for custom events) - //Screen string `json:"screen,omitempty"` // Screen resolution (ex. "1920x1080") - //Title string `json:"title,omitempty"` // Page title +type RybbitEvent struct { + APIKey string `json:"api_key"` + SiteID string `json:"site_id"` + Type string `json:"type"` + Pathname string `json:"pathname"` + Hostname string `json:"hostname,omitempty"` + IP string `json:"ip_address,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + Language string `json:"language,omitempty"` + EventName string `json:"event_name,omitempty"` + Referrer string `json:"referrer,omitempty"` + Properties string `json:"properties,omitempty"` } type SendBody struct { - Payload *UmamiEvent `json:"payload"` - Type string `json:"type"` + Payload *RybbitEvent `json:"payload"` + Type string `json:"type"` } func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) { hostname := parseDomainFromHost(req.Host) - websiteId := getWebsiteId(h, hostname) + websiteId, ok := h.websites[hostname] - if websiteId == "" { - h.error("tracking skipped, websiteId is unknown: " + hostname) + if !ok { + h.error("tracking skipped, site-id is unknown: " + hostname) return } - event := &UmamiEvent{ + rEvent := &RybbitEvent{ + APIKey: h.apiKey, + SiteID: websiteId, + Type: "pageview", + Pathname: req.URL.Path, Hostname: hostname, - Language: parseAcceptLanguage(req.Header.Get("Accept-Language")), - Referrer: req.Referer(), - Url: req.URL.String(), - Ip: extractRemoteIP(req), + IP: extractRemoteIP(req), UserAgent: req.Header.Get("User-Agent"), - Timestamp: time.Now().Unix(), - Website: websiteId, + Referrer: req.Referer(), + Language: parseAcceptLanguage(req.Header.Get("Accept-Language")), } select { - case h.queue <- event: + case h.queue <- rEvent: default: h.error("failed to submit event: queue full") } @@ -108,16 +108,18 @@ func (h *UmamiFeeder) umamiEventFeeder(ctx context.Context) (err error) { func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBody) { h.debug("reporting %d events", len(events)) - resp, err := sendRequest(ctx, h.umamiHost+"/api/batch", events, nil) - if err != nil { - h.error("failed to send tracking: " + err.Error()) - return + for _, value := range events { + resp, err := sendRequest(ctx, h.host+"/api/track", value.Payload, nil) + if err != nil { + h.error("failed to send tracking: " + err.Error()) + return + } + if h.isDebug { + bodyBytes, _ := io.ReadAll(resp.Body) + h.debug("%v: %s", resp.Status, string(bodyBytes)) + } + defer func() { + _ = resp.Body.Close() + }() } - if h.isDebug { - bodyBytes, _ := io.ReadAll(resp.Body) - h.debug("%v: %s", resp.Status, string(bodyBytes)) - } - defer func() { - _ = resp.Body.Close() - }() }