routeranalytics
packageAPI reference for the routeranalytics
package.
Imports
(7)wasmPrefetcher
type wasmPrefetcher struct
Methods
Parameters
func (*wasmPrefetcher) request(resources []string)
{
if len(resources) == 0 {
return
}
p.mu.Lock()
defer p.mu.Unlock()
if p.requested == nil {
p.requested = make(map[string]struct{})
}
filtered := make([]string, 0, len(resources))
for _, r := range resources {
if r == "" {
continue
}
if _, ok := p.requested[r]; ok {
continue
}
p.requested[r] = struct{}{}
filtered = append(filtered, r)
}
if len(filtered) == 0 {
return
}
p.tick++
// Same wire shape a netcode client would produce: hints are one-way, so
// the full client (snapshots, interpolation) is not needed here.
hostclient.Send(p.channel, map[string]any{
"tick": p.tick,
"commands": []any{map[string]any{"resources": filtered}},
})
}
func (*wasmPrefetcher) reset()
{
p.mu.Lock()
p.requested = make(map[string]struct{})
p.tick = 0
p.mu.Unlock()
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.Mutex | |
| tick | int64 | |
| requested | map[string]struct{} | |
| channel | string |
newPrefetcher
Parameters
Returns
func newPrefetcher(channel string) prefetcher
{
return &wasmPrefetcher{
channel: channel,
requested: make(map[string]struct{}),
}
}
newPrefetcher
Parameters
Returns
func newPrefetcher(string) prefetcher
{ return noopPrefetcher{} }
Options
Options configures route normalization and prefetch prediction.
type Options struct
Fields
| Name | Type | Description |
|---|---|---|
| Normalize | func(string) string | |
| PrefetchLimit | int | |
| PrefetchThreshold | float64 | |
| Channel | string |
TransitionProbability
TransitionProbability describes one observed route transition.
type TransitionProbability struct
Fields
| Name | Type | Description |
|---|---|---|
| From | string | |
| To | string | |
| Count | int | |
| Probability | float64 |
Plugin
Plugin tracks route transitions and emits prefetch hints.
type Plugin struct
Methods
Name returns the plugin name.
Returns
func (*Plugin) Name() string
{
return "routeranalytics"
}
Build performs no build-time work.
Parameters
Returns
func (*Plugin) Build(json.RawMessage) error
{ return nil }
Install registers route tracking with the application.
Parameters
func (*Plugin) Install(a *core.App)
{
if a == nil {
return
}
setDefaultInstance(p)
a.RegisterRouter(func(path string) {
p.handleNavigation(path)
})
}
TransitionProbabilities returns transitions observed after from.
Parameters
Returns
func (*Plugin) TransitionProbabilities(from string) []TransitionProbability
{
if p == nil {
return nil
}
probs := p.tracker.probabilities(from)
if len(probs) == 0 {
return nil
}
out := make([]TransitionProbability, len(probs))
copy(out, probs)
return out
}
MostLikelyNext returns the highest probability transitions after from.
Parameters
Returns
func (*Plugin) MostLikelyNext(from string, limit int) []TransitionProbability
{
if p == nil || limit <= 0 {
return nil
}
probs := p.tracker.mostLikelyNext(from, limit)
if len(probs) == 0 {
return nil
}
out := make([]TransitionProbability, len(probs))
copy(out, probs)
return out
}
Reset clears recorded route transitions.
func (*Plugin) Reset()
{
if p == nil {
return
}
p.tracker.reset()
if p.prefetch != nil {
p.prefetch.reset()
}
}
Parameters
func (*Plugin) enqueuePrefetch(current string)
{
if p.opts.PrefetchLimit <= 0 || p.prefetch == nil {
return
}
predictions := p.tracker.mostLikelyNext(current, p.opts.PrefetchLimit)
if len(predictions) == 0 {
return
}
threshold := p.opts.PrefetchThreshold
if threshold < 0 {
threshold = 0
}
resources := make([]string, 0, len(predictions))
for _, prob := range predictions {
if prob.Probability < threshold {
continue
}
resources = append(resources, prob.To)
}
if len(resources) == 0 {
return
}
p.prefetch.request(resources)
}
Fields
| Name | Type | Description |
|---|---|---|
| opts | Options | |
| tracker | *transitionTracker | |
| prefetch | prefetcher |
New
New creates a route analytics plugin.
func New(opts Options) *Plugin
{
if opts.Normalize == nil {
opts.Normalize = defaultNormalize
}
if opts.PrefetchLimit < 0 {
opts.PrefetchLimit = 0
} else if opts.PrefetchLimit == 0 {
opts.PrefetchLimit = 3
}
if opts.PrefetchThreshold <= 0 {
opts.PrefetchThreshold = 0.2
}
if opts.Channel == "" {
opts.Channel = "RouterPrefetch"
}
plugin := &Plugin{
opts: opts,
tracker: newTransitionTracker(),
}
if opts.PrefetchLimit > 0 {
plugin.prefetch = newPrefetcher(opts.Channel)
} else {
plugin.prefetch = noopPrefetcher{}
}
return plugin
}
TransitionProbabilities
TransitionProbabilities queries the default plugin instance.
Parameters
Returns
func TransitionProbabilities(from string) []TransitionProbability
{
if inst := getDefaultInstance(); inst != nil {
return inst.TransitionProbabilities(from)
}
return nil
}
MostLikelyNext
MostLikelyNext queries the default plugin instance.
Parameters
Returns
func MostLikelyNext(from string, limit int) []TransitionProbability
{
if inst := getDefaultInstance(); inst != nil {
return inst.MostLikelyNext(from, limit)
}
return nil
}
Reset
Reset clears the default plugin instance.
func Reset()
{
if inst := getDefaultInstance(); inst != nil {
inst.Reset()
}
}
transitionTracker
type transitionTracker struct
Methods
Parameters
func (*transitionTracker) visit(routeID string)
{
if routeID == "" {
return
}
t.mu.Lock()
if t.last != "" {
t.recordLocked(t.last, routeID)
}
t.last = routeID
t.mu.Unlock()
}
Parameters
func (*transitionTracker) recordLocked(from, to string)
{
if from == "" || to == "" {
return
}
if t.transitions[from] == nil {
t.transitions[from] = make(map[string]int)
}
t.transitions[from][to]++
t.totals[from]++
}
func (*transitionTracker) reset()
{
t.mu.Lock()
t.transitions = make(map[string]map[string]int)
t.totals = make(map[string]int)
t.last = ""
t.mu.Unlock()
}
Parameters
Returns
func (*transitionTracker) probabilities(from string) []TransitionProbability
{
t.mu.RLock()
total := t.totals[from]
if total == 0 {
t.mu.RUnlock()
return nil
}
counts := t.transitions[from]
result := make([]TransitionProbability, 0, len(counts))
for to, count := range counts {
result = append(result, TransitionProbability{
From: from,
To: to,
Count: count,
Probability: float64(count) / float64(total),
})
}
t.mu.RUnlock()
sort.Slice(result, func(i, j int) bool {
if result[i].Probability == result[j].Probability {
return result[i].To < result[j].To
}
return result[i].Probability > result[j].Probability
})
return result
}
Parameters
Returns
func (*transitionTracker) mostLikelyNext(from string, limit int) []TransitionProbability
{
if limit <= 0 {
return nil
}
probs := t.probabilities(from)
if len(probs) == 0 {
return nil
}
if limit >= len(probs) {
out := make([]TransitionProbability, len(probs))
copy(out, probs)
return out
}
out := make([]TransitionProbability, limit)
copy(out, probs[:limit])
return out
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.RWMutex | |
| transitions | map[string]map[string]int | |
| totals | map[string]int | |
| last | string |
newTransitionTracker
Returns
func newTransitionTracker() *transitionTracker
{
return &transitionTracker{
transitions: make(map[string]map[string]int),
totals: make(map[string]int),
}
}
defaultNormalize
Parameters
Returns
func defaultNormalize(path string) string
{
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return ""
}
if idx := strings.IndexAny(trimmed, "?#"); idx >= 0 {
trimmed = trimmed[:idx]
}
if trimmed == "" {
return ""
}
if !strings.HasPrefix(trimmed, "/") {
trimmed = "/" + trimmed
}
return trimmed
}
NormalizePath
NormalizePath applica la normalizzazione predefinita (trim, rimozione di query/hash e slash iniziale).
Parameters
Returns
func NormalizePath(path string) string
{
return defaultNormalize(path)
}
setDefaultInstance
Parameters
func setDefaultInstance(p *Plugin)
{
defaultMu.Lock()
defaultInstance = p
defaultMu.Unlock()
}
getDefaultInstance
Returns
func getDefaultInstance() *Plugin
{
defaultMu.RLock()
defer defaultMu.RUnlock()
return defaultInstance
}
TestTransitionProbabilities
Parameters
func TestTransitionProbabilities(t *testing.T)
{
p := New(Options{})
p.Reset()
p.handleNavigation("/home")
p.handleNavigation("/posts")
p.handleNavigation("/home")
p.handleNavigation("/posts")
p.handleNavigation("/settings")
p.handleNavigation("/posts")
p.handleNavigation("/home")
probs := p.TransitionProbabilities("/posts")
if len(probs) != 2 {
t.Fatalf("expected two transitions, got %d", len(probs))
}
if probs[0].To != "/home" {
t.Fatalf("expected /home to be most probable, got %s", probs[0].To)
}
if probs[0].Count != 2 || probs[1].Count != 1 {
t.Fatalf("expected counts to reflect visits, got %v", probs)
}
if got := p.TransitionProbabilities("/missing"); got != nil {
t.Fatalf("expected nil for unknown route, got %v", got)
}
}
TestMostLikelyNextOrdering
Parameters
func TestMostLikelyNextOrdering(t *testing.T)
{
p := New(Options{})
p.Reset()
p.handleNavigation("/a")
p.handleNavigation("/b")
p.handleNavigation("/c")
p.handleNavigation("/b")
p.handleNavigation("/c")
p.handleNavigation("/d")
p.handleNavigation("/b")
p.handleNavigation("/d")
probs := p.MostLikelyNext("/b", 2)
if len(probs) != 2 {
t.Fatalf("expected two results, got %d", len(probs))
}
if probs[0].To != "/c" {
t.Fatalf("expected /c to be first, got %s", probs[0].To)
}
if probs[0].Probability <= probs[1].Probability {
t.Fatalf("expected probability ordering")
}
if limited := p.MostLikelyNext("/b", 1); len(limited) != 1 || limited[0].To != "/c" {
t.Fatalf("expected limit to return most probable route")
}
if none := p.MostLikelyNext("/z", 3); none != nil {
t.Fatalf("expected nil for unknown transitions, got %v", none)
}
}
TestDefaultNormalize
Parameters
func TestDefaultNormalize(t *testing.T)
{
p := New(Options{})
normalized := p.opts.Normalize(" docs/view?id=7#section ")
if normalized != "/docs/view" {
t.Fatalf("expected normalized path, got %q", normalized)
}
if global := NormalizePath(" docs/view?id=7#section "); global != normalized {
t.Fatalf("expected NormalizePath to match default normalize, got %q", global)
}
}