Implement Rybbit changes
This commit is contained in:
parent
8d7198941f
commit
6def58012a
14 changed files with 142 additions and 345 deletions
BIN
.assets/icon.png
BIN
.assets/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 292 KiB |
16
.traefik.yml
16
.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
|||
module github.com/astappiev/traefik-umami-feeder
|
||||
module github.com/FoxxMD/traefik-rybbit-feeder
|
||||
|
||||
go 1.21
|
||||
|
|
|
|||
71
readme.md
71
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,23 +91,18 @@ 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. |
|
||||
| `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"]`). |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package traefik_umami_feeder
|
||||
package traefik_rybbit_feeder
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
|
|
|||
105
umami.go
105
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,
|
||||
|
||||
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,
|
||||
host: config.Host,
|
||||
apiKey: config.APIKey,
|
||||
websites: config.Websites,
|
||||
websitesMutex: sync.RWMutex{},
|
||||
createNewWebsites: config.CreateNewWebsites,
|
||||
|
||||
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 h.apiKey == "" {
|
||||
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 {
|
||||
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.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 fmt.Errorf("Failed to get health for rybbit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -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) {})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package traefik_umami_feeder
|
||||
package traefik_rybbit_feeder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
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,7 +108,8 @@ 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)
|
||||
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
|
||||
|
|
@ -121,3 +122,4 @@ func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBod
|
|||
_ = resp.Body.Close()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue