Implement Rybbit changes

This commit is contained in:
FoxxMD 2025-07-29 15:34:57 -04:00
parent 8d7198941f
commit 6def58012a
14 changed files with 142 additions and 345 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Before After
Before After

View file

@ -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 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 iconPath: .assets/icon.png
testData: testData:
umamiHost: "" Host: ""
umamiToken: "" ApiKey: ""
umamiUsername: ""
umamiPassword: ""
umamiTeamId: ""
websites: websites:
domain: websiteId localhost: siteId
createNewWebsites: false

View file

@ -1,11 +1,11 @@
http: http:
middlewares: middlewares:
umami: rybbit:
plugin: plugin:
umami-feeder: rybbit-feeder:
debug: true debug: true
umamiHost: http://umami:3000 host: https://rybbit.mysite.com
umamiUsername: "admin" apiKey: rb_a0938f333a88239c6250c2c3707a
umamiPassword: "umami" websites:
umamiTeamId: "8e39c6ad-e44a-4d3e-be98-015db2d62d40" "localhost": "1"
createNewWebsites: true batchSize: 1

View file

@ -1,8 +1,6 @@
services: services:
traefik: traefik:
image: traefik:3 image: traefik:3
depends_on:
- umami
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.traefik.service=api@internal" - "traefik.http.routers.traefik.service=api@internal"
@ -24,7 +22,7 @@ services:
- ./traefik.yml:/traefik.yml:ro - ./traefik.yml:/traefik.yml:ro
- ./config.yml:/config/config.yml:ro - ./config.yml:/config/config.yml:ro
# mount plugin # mount plugin
- ../:/plugins-local/src/github.com/astappiev/traefik-umami-feeder - ../:/plugins-local/src/github.com/foxxmd/traefik-rybbit-feeder
restart: unless-stopped restart: unless-stopped
@ -32,7 +30,7 @@ services:
image: containous/whoami image: containous/whoami
ports: ports:
- target: 80 - target: 80
published: 80 published: 7654
protocol: tcp protocol: tcp
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@ -41,44 +39,10 @@ services:
- "traefik.http.routers.whoami.rule=Host(`localhost`)" - "traefik.http.routers.whoami.rule=Host(`localhost`)"
- "traefik.http.routers.whoami.entrypoints=whoami" - "traefik.http.routers.whoami.entrypoints=whoami"
# middleware # middleware
- "traefik.http.routers.whoami.middlewares=umami@file" - "traefik.http.routers.whoami.middlewares=rybbit@file"
networks: networks:
- traefik - 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: networks:
traefik: traefik:
umami_postgres: name: traefik

View file

@ -18,13 +18,11 @@ entryPoints:
address: ":8080" address: ":8080"
"whoami": # http "whoami": # http
address: ":8081" address: ":8081"
"umami": # umami dashboard
address: ":3000"
api: api:
dashboard: true dashboard: true
experimental: experimental:
localPlugins: localPlugins:
umami-feeder: rybbit-feeder:
moduleName: "github.com/astappiev/traefik-umami-feeder" moduleName: "github.com/foxxmd/traefik-rybbit-feeder"

2
go.mod
View file

@ -1,3 +1,3 @@
module github.com/astappiev/traefik-umami-feeder module github.com/FoxxMD/traefik-rybbit-feeder
go 1.21 go 1.21

View file

@ -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 **This is a fork of [traefik-umami-feeder](https://github.com/astappiev/traefik-umami-feeder) modified to work with Rybbit.**
inspired by the [Plausible Feeder Traefik Plugin](https://github.com/safing/plausiblefeeder).
## Introduction ## Introduction
This plugin integrates your Traefik-proxied services with Umami, a simple, fast, privacy-focused analytics solution. It 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, screen size, IP) and forwards it to your Umami instance, captures basic request information (path, user-agent, referrer, IP) and forwards it to your Rybbit instance,
enabling server-side analytics. enabling server-side analytics.
Key features: Key features:
@ -26,51 +25,42 @@ Declare the plugin in your Traefik **static configuration**.
```yaml ```yaml
experimental: experimental:
plugins: plugins:
umami-feeder: rybbit-feeder:
moduleName: github.com/astappiev/traefik-umami-feeder moduleName: github.com/foxxmd/traefik-rybbit-feeder
version: v1.3.0 # Replace with the latest version version: v0.13.0 # Replace with the latest version
``` ```
### Step 2. Configure the middleware ### Step 2. Configure the middleware
Once the plugin is declared, configure it as a middleware in your Traefik **dynamic configuration**. 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. * `host` - Your Rybbit instance base URL. This is URL **without** `/api/track`.
2. **Automatic**: Configure the plugin with your Umami API `umamiToken`, or `umamiUsername` and `umamiPassword`. The * `apiKey` - The [**Api Key**](https://www.rybbit.io/docs/api#steps) generated for the website you wish to track.
plugin will then automatically fetch the list of websites and their IDs from your Umami instance. * `websites` - A map of `domain` - `site-id` properties to tell the feeder what traffic should go to your Rybbit Site
* 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.
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 ```yaml
http: http:
middlewares: middlewares:
my-umami-middleware: my-rybbit-middleware:
plugin: plugin:
umami-feeder: rybbit-feeder:
umamiHost: "http://umami:3000" # URL of your Umami instance host: "http://rybbit.mysite.com" # URL of your Rybbit instance
# Option 1: Define the list of websites apiKey: rb_a0938250c2c2efd061c8250c2c3707a
# websites:
# "example.com": "d4617504-241c-4797-8eab-5939b367b3ad"
# Option 2: Use Umami credentials to fetch websites websites:
umamiUsername: "your-umami-username" # domain to capture traffic for and the site-id from Rybbit
umamiPassword: "your-umami-password" "example.com": "1"
# umamiToken: "your-umami-api-token" # Alternative to username/password
# Optional: allow creation of new websites in Umami
createNewWebsites: true
``` ```
### Step 3. Attach the middleware to your routers ### 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 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 Remember to use the
correct [provider namespace](https://doc.traefik.io/traefik/providers/overview/#provider-namespace) (e.g., `@file` if 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:** **Example using Docker labels:**
```yaml ```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`):** **Example using a dynamic configuration file (e.g., `dynamic_conf.yml`):**
@ -90,7 +80,7 @@ http:
whoami: whoami:
rule: "Host(`example.com`)" rule: "Host(`example.com`)"
middlewares: 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 **Example using static configuration (e.g., `traefik.yml`), by attaching the middleware to an entryPoint to apply it
@ -101,23 +91,18 @@ entryPoints:
web: web:
http: http:
middlewares: middlewares:
- my-umami-middleware@file - my-rybbit-middleware@file
``` ```
## Middleware Options ## Middleware Options
| key | default | type | description | | key | default | type | description |
|---------------------|-----------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ------------------- | :-------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | `false` | `bool` | Set to `true` to disable the plugin. | | `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. | | `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. | | `host` | **required** | `string` | URL of your Rybbit instance, reachable from Traefik (e.g., `https://rybbit.mydomain.com`). |
| `umamiHost` | **required** | `string` | URL of your Umami instance, reachable from Traefik (e.g., `http://umami:3000`). | | `apiKey` | **required** | `string` | [Rybbit API Key](https://www.rybbit.io/docs/api#steps) for authenticating with your Rybbit instance. |
| `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. | | `websites` | **required** | `map` | A map of `hostname: site-id` |
| `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). | | `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. | | `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"]`). | | `trackExtensions` | `[see sources]` | `string[]` | A list of specific file extensions to track (e.g., `[".html", ".php"]`). |

View file

@ -1,4 +1,4 @@
package traefik_umami_feeder package traefik_rybbit_feeder
import ( import (
"bufio" "bufio"

105
umami.go
View file

@ -1,4 +1,4 @@
package traefik_umami_feeder package traefik_rybbit_feeder
import ( import (
"context" "context"
@ -21,29 +21,20 @@ type Config struct {
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
// Debug enables debug logging, be prepared for flooding. // Debug enables debug logging, be prepared for flooding.
Debug bool `json:"debug"` 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"` 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"` 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"` BatchMaxWait time.Duration `json:"batchMaxWait"`
// UmamiHost is the URL of the Umami instance. // Host is the URL of the Rybbit instance.
UmamiHost string `json:"umamiHost"` Host string `json:"host"`
// UmamiToken is an API KEY, which is optional, but either UmamiToken or Websites should be set. // APIKey is the API Key generated in Site Settings for a Rybbit Website
UmamiToken string `json:"umamiToken"` APIKey string `json:"apiKey"`
// 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"`
// Websites is a map of domain to websiteId, which is required if UmamiToken is not set. // Websites is a map of domain to site-id, which is required
// 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"`
// 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 defines whether errors (status codes >= 400) should be tracked.
TrackErrors bool `json:"trackErrors"` TrackErrors bool `json:"trackErrors"`
@ -73,14 +64,10 @@ func CreateConfig() *Config {
BatchMaxWait: 5 * time.Second, BatchMaxWait: 5 * time.Second,
TrackErrors: false, TrackErrors: false,
UmamiHost: "", Host: "",
UmamiToken: "", APIKey: "",
UmamiUsername: "",
UmamiPassword: "",
UmamiTeamId: "",
Websites: map[string]string{}, Websites: map[string]string{},
CreateNewWebsites: false,
TrackAllResources: false, TrackAllResources: false,
TrackExtensions: []string{}, TrackExtensions: []string{},
@ -99,14 +86,13 @@ type UmamiFeeder struct {
isDebug bool isDebug bool
isDisabled bool isDisabled bool
logHandler *log.Logger logHandler *log.Logger
queue chan *UmamiEvent queue chan *RybbitEvent
batchSize int batchSize int
batchMaxWait time.Duration batchMaxWait time.Duration
umamiHost string host string
umamiToken string apiKey string
umamiTeamId string
websites map[string]string websites map[string]string
websitesMutex sync.RWMutex websitesMutex sync.RWMutex
createNewWebsites bool createNewWebsites bool
@ -131,16 +117,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
isDisabled: config.Disabled, isDisabled: config.Disabled,
logHandler: log.New(os.Stdout, "", 0), logHandler: log.New(os.Stdout, "", 0),
queue: make(chan *UmamiEvent, config.QueueSize), queue: make(chan *RybbitEvent, config.QueueSize),
batchSize: config.BatchSize, batchSize: config.BatchSize,
batchMaxWait: config.BatchMaxWait, batchMaxWait: 1 * time.Second,
umamiHost: config.UmamiHost, host: config.Host,
umamiToken: config.UmamiToken, apiKey: config.APIKey,
umamiTeamId: config.UmamiTeamId,
websites: config.Websites, websites: config.Websites,
websitesMutex: sync.RWMutex{}, websitesMutex: sync.RWMutex{},
createNewWebsites: config.CreateNewWebsites,
trackErrors: config.TrackErrors, trackErrors: config.TrackErrors,
trackAllResources: config.TrackAllResources, trackAllResources: config.TrackAllResources,
@ -154,6 +138,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
if !h.isDisabled { if !h.isDisabled {
h.isDisabled = true h.isDisabled = true
h.debug("batchSize %d", h.batchSize)
h.debug("batchMaxWait %v", h.batchMaxWait)
go h.retryConnection(ctx, config) go h.retryConnection(ctx, config)
} }
@ -178,11 +164,11 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) {
select { select {
case <-time.After(currentDelay): case <-time.After(currentDelay):
retryAttempt++ 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) err := h.connect(ctx, config)
if err == nil { if err == nil {
h.debug("Successfully connected to Umami. Verifying configuration...") h.debug("Successfully connected to Rybbit. Verifying configuration...")
err = h.verifyConfig(config) err = h.verifyConfig(config)
if err == nil { if err == nil {
@ -197,7 +183,7 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) {
return // Exit retry goroutine, plugin remains disabled. 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(): case <-ctx.Done():
h.debug("Context cancelled during retryConnection, stopping connection retries.") h.debug("Context cancelled during retryConnection, stopping connection retries.")
return return
@ -206,42 +192,21 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) {
} }
func (h *UmamiFeeder) connect(ctx context.Context, config *Config) error { func (h *UmamiFeeder) connect(ctx context.Context, config *Config) error {
if h.umamiHost == "" { if h.host == "" {
return fmt.Errorf("`umamiHost` is not set") return fmt.Errorf("`host` is not set")
} }
if config.UmamiUsername != "" && config.UmamiPassword != "" { if h.apiKey == "" {
token, err := getToken(ctx, h.umamiHost, config.UmamiUsername, config.UmamiPassword) return fmt.Errorf("`apiKey` should be set")
}
if len(h.websites) == 0 {
return fmt.Errorf("`websites` should not be empty")
}
_, err := sendRequest(ctx, h.host+"/health", nil, nil)
if err != nil { if err != nil {
return fmt.Errorf("failed to get token: %w", err) return fmt.Errorf("Failed to get health for rybbit: %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.umamiToken != "" {
websites, err := fetchWebsites(ctx, h.umamiHost, h.umamiToken, h.umamiTeamId)
if err != nil {
return fmt.Errorf("failed to fetch websites: %w", err)
}
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)
} }
return nil return nil

View file

@ -1,4 +1,4 @@
package traefik_umami_feeder package traefik_rybbit_feeder
import ( import (
"context" "context"
@ -9,11 +9,11 @@ import (
func TestTraefikUmamiFeeder(t *testing.T) { func TestTraefikUmamiFeeder(t *testing.T) {
cfg := CreateConfig() cfg := CreateConfig()
cfg.UmamiHost = "http://localhost:3000" cfg.Host = "http://localhost:3000"
cfg.UmamiUsername = "admin" cfg.APIKey = "8e39c6ad-e44a-4d3e-be98-015db2d62d40"
cfg.UmamiPassword = "umami" cfg.Websites = map[string]string{
cfg.UmamiTeamId = "8e39c6ad-e44a-4d3e-be98-015db2d62d40" "localhost": "4",
cfg.CreateNewWebsites = true }
ctx := context.Background() ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})

View file

@ -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
}

View file

@ -1,4 +1,4 @@
package traefik_umami_feeder package traefik_rybbit_feeder
import ( import (
"bytes" "bytes"

View file

@ -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
}

View file

@ -1,4 +1,4 @@
package traefik_umami_feeder package traefik_rybbit_feeder
import ( import (
"context" "context"
@ -8,48 +8,48 @@ import (
"time" "time"
) )
type UmamiEvent struct { type RybbitEvent struct {
Website string `json:"website"` // Website ID APIKey string `json:"api_key"`
Hostname string `json:"hostname"` // Name of host SiteID string `json:"site_id"`
Language string `json:"language,omitempty"` // Language of visitor (ex. "en-US") Type string `json:"type"`
Referrer string `json:"referrer,omitempty"` // Referrer URL Pathname string `json:"pathname"`
Url string `json:"url"` // Page URL Hostname string `json:"hostname,omitempty"`
Ip string `json:"ip,omitempty"` // IP address IP string `json:"ip_address,omitempty"`
UserAgent string `json:"userAgent,omitempty"` // User agent UserAgent string `json:"user_agent,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"` // UNIX timestamp in seconds Language string `json:"language,omitempty"`
//Data map[string]interface{} `json:"data,omitempty"` // Additional data for the event EventName string `json:"event_name,omitempty"`
//Name string `json:"name,omitempty"` // Event name (for custom events) Referrer string `json:"referrer,omitempty"`
//Screen string `json:"screen,omitempty"` // Screen resolution (ex. "1920x1080") Properties string `json:"properties,omitempty"`
//Title string `json:"title,omitempty"` // Page title
} }
type SendBody struct { type SendBody struct {
Payload *UmamiEvent `json:"payload"` Payload *RybbitEvent `json:"payload"`
Type string `json:"type"` Type string `json:"type"`
} }
func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) { func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) {
hostname := parseDomainFromHost(req.Host) hostname := parseDomainFromHost(req.Host)
websiteId := getWebsiteId(h, hostname) websiteId, ok := h.websites[hostname]
if websiteId == "" { if !ok {
h.error("tracking skipped, websiteId is unknown: " + hostname) h.error("tracking skipped, site-id is unknown: " + hostname)
return return
} }
event := &UmamiEvent{ rEvent := &RybbitEvent{
APIKey: h.apiKey,
SiteID: websiteId,
Type: "pageview",
Pathname: req.URL.Path,
Hostname: hostname, Hostname: hostname,
Language: parseAcceptLanguage(req.Header.Get("Accept-Language")), IP: extractRemoteIP(req),
Referrer: req.Referer(),
Url: req.URL.String(),
Ip: extractRemoteIP(req),
UserAgent: req.Header.Get("User-Agent"), UserAgent: req.Header.Get("User-Agent"),
Timestamp: time.Now().Unix(), Referrer: req.Referer(),
Website: websiteId, Language: parseAcceptLanguage(req.Header.Get("Accept-Language")),
} }
select { select {
case h.queue <- event: case h.queue <- rEvent:
default: default:
h.error("failed to submit event: queue full") h.error("failed to submit event: queue full")
} }
@ -108,7 +108,8 @@ func (h *UmamiFeeder) umamiEventFeeder(ctx context.Context) (err error) {
func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBody) { func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBody) {
h.debug("reporting %d events", len(events)) h.debug("reporting %d events", len(events))
resp, err := sendRequest(ctx, h.umamiHost+"/api/batch", events, nil) for _, value := range events {
resp, err := sendRequest(ctx, h.host+"/api/track", value.Payload, nil)
if err != nil { if err != nil {
h.error("failed to send tracking: " + err.Error()) h.error("failed to send tracking: " + err.Error())
return return
@ -120,4 +121,5 @@ func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBod
defer func() { defer func() {
_ = resp.Body.Close() _ = resp.Body.Close()
}() }()
}
} }