This commit is contained in:
ponzischeme89
2026-08-24 22:56:46 +12:00
parent 4f95767e2f
commit 396d35e2f5
48 changed files with 1541 additions and 672 deletions
+92
View File
@@ -97,6 +97,30 @@ type FeatureStat struct {
LastUsedAt time.Time `json:"lastUsedAt"`
}
// JourneyFeatureStat is the server-derived usage summary for one feature. Users are
// distinct profiles, Uses are recorded feature events, and Journeys are distinct app
// visits containing the feature.
type JourneyFeatureStat struct {
Feature string `json:"feature"`
Users int64 `json:"users"`
Uses int64 `json:"uses"`
Journeys int64 `json:"journeys"`
ActiveUserRate float64 `json:"activeUserRate"`
LastUsedAt time.Time `json:"lastUsedAt"`
}
// JourneyBreakdownStat keeps the three telemetry dimensions separate. SubFeature is
// sourced from the structured entry/source/target context, never parsed from display text.
type JourneyBreakdownStat struct {
Feature string `json:"feature"`
SubFeature string `json:"subFeature"`
Action string `json:"action"`
Users int64 `json:"users"`
Uses int64 `json:"uses"`
Journeys int64 `json:"journeys"`
LastUsedAt time.Time `json:"lastUsedAt"`
}
type PathStat struct {
From string `json:"from"`
To string `json:"to"`
@@ -430,6 +454,74 @@ func (s *Store) UserFeatureStats(ctx context.Context, userID string, since time.
return out, rows.Err()
}
// JourneyFeatureUsage aggregates structured journey telemetry. Lifecycle markers are not
// feature usage: screen views and feature actions are retained so a feature can be viewed
// without being selected, requested or played.
func (s *Store) JourneyFeatureUsage(ctx context.Context, userID string, since time.Time) ([]JourneyFeatureStat, error) {
rows, err := s.pool.Query(ctx, `
WITH active AS (
SELECT count(DISTINCT emby_user_id) AS users
FROM journey_events WHERE occurred_at >= $1
), events AS (
SELECT coalesce(nullif(feature, ''), nullif(category, '')) AS feature,
emby_user_id, journey_id, occurred_at
FROM journey_events
WHERE occurred_at >= $1 AND ($2 = '' OR emby_user_id = $2)
AND action NOT IN ('journey_start', 'journey_end')
)
SELECT feature, count(DISTINCT emby_user_id), count(*), count(DISTINCT (emby_user_id, journey_id)),
coalesce(count(DISTINCT emby_user_id)::double precision /
nullif((SELECT users FROM active), 0), 0), max(occurred_at)
FROM events
WHERE feature IS NOT NULL AND feature <> ''
GROUP BY feature ORDER BY count(*) DESC, feature`, since, userID)
if err != nil {
return nil, fmt.Errorf("store: journey feature usage: %w", err)
}
defer rows.Close()
out := []JourneyFeatureStat{}
for rows.Next() {
var value JourneyFeatureStat
if err := rows.Scan(&value.Feature, &value.Users, &value.Uses, &value.Journeys,
&value.ActiveUserRate, &value.LastUsedAt); err != nil {
return nil, err
}
out = append(out, value)
}
return out, rows.Err()
}
// JourneyFeatureBreakdown is the drill-down behind the feature summary. Source is the
// strongest sub-feature signal (for example text or voice search); target and screen are
// fallbacks for older structured events.
func (s *Store) JourneyFeatureBreakdown(ctx context.Context, userID string, since time.Time) ([]JourneyBreakdownStat, error) {
rows, err := s.pool.Query(ctx, `
SELECT coalesce(nullif(feature, ''), nullif(category, '')) AS feature,
coalesce(nullif(source, ''), nullif(target, ''), nullif(screen, ''), '') AS sub_feature,
action, count(DISTINCT emby_user_id), count(*), count(DISTINCT (emby_user_id, journey_id)), max(occurred_at)
FROM journey_events
WHERE occurred_at >= $1 AND ($2 = '' OR emby_user_id = $2)
AND action NOT IN ('journey_start', 'journey_end')
AND coalesce(nullif(feature, ''), nullif(category, '')) IS NOT NULL
GROUP BY coalesce(nullif(feature, ''), nullif(category, '')),
coalesce(nullif(source, ''), nullif(target, ''), nullif(screen, ''), ''), action
ORDER BY count(*) DESC, feature, sub_feature, action`, since, userID)
if err != nil {
return nil, fmt.Errorf("store: journey feature breakdown: %w", err)
}
defer rows.Close()
out := []JourneyBreakdownStat{}
for rows.Next() {
var value JourneyBreakdownStat
if err := rows.Scan(&value.Feature, &value.SubFeature, &value.Action, &value.Users,
&value.Uses, &value.Journeys, &value.LastUsedAt); err != nil {
return nil, err
}
out = append(out, value)
}
return out, rows.Err()
}
func (s *Store) UserPaths(ctx context.Context, userID string, since time.Time) ([]PathStat, error) {
rows, err := s.pool.Query(ctx, `
WITH ordered AS (