0.3.21
This commit is contained in:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user