ahoy_analytics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +163 -0
- data/Rakefile +6 -0
- data/app/assets/ahoy_analytics/build/assets/Combination-BpSXUjp9.js +41 -0
- data/app/assets/ahoy_analytics/build/assets/analytics-5KyfCxh6.css +1 -0
- data/app/assets/ahoy_analytics/build/assets/analytics-dashboard-uOXx8zYZ.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/analytics-layout-ClAft5OU.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/analytics-tracker-B3f8P98z.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/analytics-ui-DMSkNqd6.js +90 -0
- data/app/assets/ahoy_analytics/build/assets/behaviors-panel-ChNGYbdH.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/button-JVCrlR4s.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/cable-DO-7y1-E.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/createLucideIcon-BGzacY2v.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/date-range-dialog-DWDp3cLG.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/details-button-NqKfSGEG.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/devices-panel-cXvlmNBY.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/dialog-path-BBPNlB4Z.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/dropdown-menu-Adj3O5fh.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/filter-dialog-BN-rf4lp.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/index-B1K1NTKT.js +3 -0
- data/app/assets/ahoy_analytics/build/assets/index-BcHeb-Rh.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/index-DzpzLoG4.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/index-vX97OY1J.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/input-e4v_v0kE.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/jsx-runtime-u17CrQMm.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/last-load-context-De5uA95L.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/list-table-ChHEzzF9.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/live-Cp2MHECh.js +2 -0
- data/app/assets/ahoy_analytics/build/assets/locations-panel-BaISRmaQ.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/mercator-BnxX5RzL.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/pages-panel-Bh25L8mP.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/panel-tabs-B2kvGFJx.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/query-context-B-PgE00D.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/remote-details-dialog-DDTcKaM5.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/show-CCRicksg.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/simple-tabs-D6G6Bs0k.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/site-context-BNteYRlR.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/sources-panel-DyB21hxD.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/top-bar-FSiLBjq6.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/top-stats-context-DU15P9jS.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/use-debounce-VBpXQRL8.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/user-context-DbYteluY.js +1 -0
- data/app/assets/ahoy_analytics/build/assets/visitor-globe-BWLDihid.js +4789 -0
- data/app/assets/ahoy_analytics/build/assets/visitor-graph-uKXjLvcu.js +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/brave.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/chrome.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/chromium.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/duckduckgo.svg +2151 -0
- data/app/assets/ahoy_analytics/images/icon/browser/edge.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/fallback.svg +5 -0
- data/app/assets/ahoy_analytics/images/icon/browser/firefox.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/opera.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/safari.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/browser/samsung-internet.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/uc.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/vivaldi.svg +1 -0
- data/app/assets/ahoy_analytics/images/icon/browser/yandex.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/android.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/chrome_os.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/fallback.svg +5 -0
- data/app/assets/ahoy_analytics/images/icon/os/fedora.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/freebsd.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/gnu_linux.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/ios.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/ipad_os.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/mac.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/ubuntu.png +0 -0
- data/app/assets/ahoy_analytics/images/icon/os/windows.png +0 -0
- data/app/assets/stylesheets/ahoy_analytics/application.css +15 -0
- data/app/channels/ahoy_analytics/analytics_channel.rb +9 -0
- data/app/controllers/ahoy_analytics/analytics_controller.rb +9 -0
- data/app/controllers/ahoy_analytics/application_controller.rb +8 -0
- data/app/controllers/ahoy_analytics/assets_controller.rb +46 -0
- data/app/controllers/ahoy_analytics/base_controller.rb +285 -0
- data/app/controllers/ahoy_analytics/behaviors_controller.rb +14 -0
- data/app/controllers/ahoy_analytics/devices_controller.rb +14 -0
- data/app/controllers/ahoy_analytics/export_controller.rb +12 -0
- data/app/controllers/ahoy_analytics/live_controller.rb +9 -0
- data/app/controllers/ahoy_analytics/locations_controller.rb +14 -0
- data/app/controllers/ahoy_analytics/main_graph_controller.rb +10 -0
- data/app/controllers/ahoy_analytics/pages_controller.rb +14 -0
- data/app/controllers/ahoy_analytics/referrers_controller.rb +34 -0
- data/app/controllers/ahoy_analytics/search_terms_controller.rb +47 -0
- data/app/controllers/ahoy_analytics/sources_controller.rb +14 -0
- data/app/controllers/ahoy_analytics/top_stats_controller.rb +13 -0
- data/app/controllers/concerns/ahoy_analytics/set_current_request.rb +17 -0
- data/app/frontend/components/analytics/hex-highlights.tsx +165 -0
- data/app/frontend/components/analytics/hex-land-layer.tsx +61 -0
- data/app/frontend/components/analytics/metric-card.tsx +138 -0
- data/app/frontend/components/analytics/sessions-by-location.tsx +62 -0
- data/app/frontend/components/analytics/visitor-globe.tsx +424 -0
- data/app/frontend/components/ui/accordion.tsx +64 -0
- data/app/frontend/components/ui/alert.tsx +66 -0
- data/app/frontend/components/ui/avatar.tsx +53 -0
- data/app/frontend/components/ui/badge.tsx +46 -0
- data/app/frontend/components/ui/button.tsx +62 -0
- data/app/frontend/components/ui/calendar.tsx +212 -0
- data/app/frontend/components/ui/card.tsx +91 -0
- data/app/frontend/components/ui/checkbox.tsx +32 -0
- data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
- data/app/frontend/components/ui/input.tsx +21 -0
- data/app/frontend/components/ui/label.tsx +22 -0
- data/app/frontend/components/ui/popover.tsx +46 -0
- data/app/frontend/components/ui/select.tsx +183 -0
- data/app/frontend/components/ui/separator.tsx +26 -0
- data/app/frontend/components/ui/sheet.tsx +139 -0
- data/app/frontend/components/ui/sidebar.tsx +726 -0
- data/app/frontend/components/ui/skeleton.tsx +13 -0
- data/app/frontend/components/ui/sonner.tsx +33 -0
- data/app/frontend/components/ui/tooltip.tsx +59 -0
- data/app/frontend/data/countries-110m.json +1 -0
- data/app/frontend/data/globe-data.json +1 -0
- data/app/frontend/entrypoints/analytics-tracker.ts +680 -0
- data/app/frontend/entrypoints/analytics-ui.tsx +26 -0
- data/app/frontend/entrypoints/analytics.css +77 -0
- data/app/frontend/layouts/analytics-layout.tsx +28 -0
- data/app/frontend/lib/cable.ts +13 -0
- data/app/frontend/lib/geocode.ts +65 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/pages/admin/analytics/api.ts +221 -0
- data/app/frontend/pages/admin/analytics/hooks/use-debounce.ts +36 -0
- data/app/frontend/pages/admin/analytics/last-load-context.tsx +29 -0
- data/app/frontend/pages/admin/analytics/lib/base-path.ts +28 -0
- data/app/frontend/pages/admin/analytics/lib/dialog-path.ts +242 -0
- data/app/frontend/pages/admin/analytics/lib/number-formatter.ts +100 -0
- data/app/frontend/pages/admin/analytics/live.tsx +608 -0
- data/app/frontend/pages/admin/analytics/query-context.tsx +61 -0
- data/app/frontend/pages/admin/analytics/show.tsx +40 -0
- data/app/frontend/pages/admin/analytics/site-context.tsx +22 -0
- data/app/frontend/pages/admin/analytics/top-stats-context.tsx +37 -0
- data/app/frontend/pages/admin/analytics/types.ts +161 -0
- data/app/frontend/pages/admin/analytics/ui/analytics-dashboard.tsx +60 -0
- data/app/frontend/pages/admin/analytics/ui/behaviors-panel.tsx +456 -0
- data/app/frontend/pages/admin/analytics/ui/date-range-dialog.tsx +173 -0
- data/app/frontend/pages/admin/analytics/ui/details-button.tsx +33 -0
- data/app/frontend/pages/admin/analytics/ui/devices-panel.tsx +474 -0
- data/app/frontend/pages/admin/analytics/ui/filter-dialog.tsx +558 -0
- data/app/frontend/pages/admin/analytics/ui/list-table.tsx +346 -0
- data/app/frontend/pages/admin/analytics/ui/locations-panel.tsx +566 -0
- data/app/frontend/pages/admin/analytics/ui/pages-panel.tsx +207 -0
- data/app/frontend/pages/admin/analytics/ui/panel-tabs.tsx +65 -0
- data/app/frontend/pages/admin/analytics/ui/remote-details-dialog.tsx +356 -0
- data/app/frontend/pages/admin/analytics/ui/simple-tabs.tsx +54 -0
- data/app/frontend/pages/admin/analytics/ui/sources-panel.tsx +771 -0
- data/app/frontend/pages/admin/analytics/ui/top-bar.tsx +793 -0
- data/app/frontend/pages/admin/analytics/ui/visitor-graph.tsx +891 -0
- data/app/frontend/pages/admin/analytics/user-context.tsx +22 -0
- data/app/frontend/styles/shared.css +156 -0
- data/app/helpers/ahoy_analytics/application_helper.rb +96 -0
- data/app/jobs/ahoy_analytics/application_job.rb +4 -0
- data/app/jobs/ahoy_analytics/update_job.rb +12 -0
- data/app/mailers/ahoy_analytics/application_mailer.rb +6 -0
- data/app/models/ahoy/event/filters.rb +7 -0
- data/app/models/ahoy/event.rb +9 -0
- data/app/models/ahoy/visit/cache_key.rb +15 -0
- data/app/models/ahoy/visit/constants.rb +11 -0
- data/app/models/ahoy/visit/devices.rb +144 -0
- data/app/models/ahoy/visit/export.rb +24 -0
- data/app/models/ahoy/visit/filters.rb +286 -0
- data/app/models/ahoy/visit/imports.rb +36 -0
- data/app/models/ahoy/visit/locations.rb +276 -0
- data/app/models/ahoy/visit/metrics.rb +473 -0
- data/app/models/ahoy/visit/ordering.rb +110 -0
- data/app/models/ahoy/visit/pages.rb +533 -0
- data/app/models/ahoy/visit/pagination.rb +17 -0
- data/app/models/ahoy/visit/ranges.rb +227 -0
- data/app/models/ahoy/visit/series.rb +177 -0
- data/app/models/ahoy/visit/sources.rb +418 -0
- data/app/models/ahoy/visit/url_labels.rb +32 -0
- data/app/models/ahoy/visit.rb +143 -0
- data/app/models/ahoy_analytics/application_record.rb +5 -0
- data/app/models/ahoy_analytics/current.rb +8 -0
- data/app/models/ahoy_analytics/funnel.rb +16 -0
- data/app/models/ahoy_analytics/imported_entry_page.rb +5 -0
- data/app/models/ahoy_analytics/imported_exit_page.rb +5 -0
- data/app/models/ahoy_analytics/imported_page.rb +5 -0
- data/app/models/ahoy_analytics/live_stats.rb +152 -0
- data/app/models/ahoy_analytics/setting.rb +19 -0
- data/app/models/analytics/source_catalog.rb +48 -0
- data/app/views/layouts/ahoy_analytics/application.html.erb +15 -0
- data/config/routes.rb +21 -0
- data/config/vite.json +22 -0
- data/db/migrate/20251006104056_create_ahoy_visits_and_events.rb +62 -0
- data/db/migrate/20251006105012_add_analytics_fields_to_ahoy_visits.rb +11 -0
- data/db/migrate/20251012090000_create_analytics_funnels_and_imports.rb +52 -0
- data/db/migrate/20251013021500_add_analytics_indexes.rb +14 -0
- data/lib/ahoy_analytics/ahoy_store.rb +429 -0
- data/lib/ahoy_analytics/asset_manifest.rb +56 -0
- data/lib/ahoy_analytics/device_bucket.rb +39 -0
- data/lib/ahoy_analytics/engine.rb +55 -0
- data/lib/ahoy_analytics/maxmind_geo.rb +77 -0
- data/lib/ahoy_analytics/version.rb +3 -0
- data/lib/ahoy_analytics.rb +52 -0
- data/lib/generators/ahoy_analytics/install/install_generator.rb +111 -0
- data/lib/generators/ahoy_analytics/install/templates/initializer.rb +28 -0
- data/lib/tasks/ahoy_analytics_tasks.rake +4 -0
- metadata +352 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Central helpers for building and parsing analytics dialog deep-links
|
|
2
|
+
import { analyticsBasePath } from './base-path'
|
|
3
|
+
|
|
4
|
+
export type SourcesMode = 'channels' | 'all' | 'utm-medium' | 'utm-source' | 'utm-campaign' | 'utm-content' | 'utm-term'
|
|
5
|
+
|
|
6
|
+
export type DialogSegment =
|
|
7
|
+
| 'channels'
|
|
8
|
+
| 'sources'
|
|
9
|
+
| 'utm_mediums'
|
|
10
|
+
| 'utm_sources'
|
|
11
|
+
| 'utm_campaigns'
|
|
12
|
+
| 'utm_contents'
|
|
13
|
+
| 'utm_terms'
|
|
14
|
+
| 'pages'
|
|
15
|
+
| 'entry-pages'
|
|
16
|
+
| 'exit-pages'
|
|
17
|
+
| 'browsers'
|
|
18
|
+
| 'operating-systems'
|
|
19
|
+
| 'screen-sizes'
|
|
20
|
+
| 'countries'
|
|
21
|
+
| 'regions'
|
|
22
|
+
| 'cities'
|
|
23
|
+
| 'devices'
|
|
24
|
+
| 'locations'
|
|
25
|
+
| 'behaviors'
|
|
26
|
+
|
|
27
|
+
const MODE_TO_SEGMENT: Record<SourcesMode, DialogSegment> = {
|
|
28
|
+
channels: 'channels',
|
|
29
|
+
all: 'sources',
|
|
30
|
+
'utm-medium': 'utm_mediums',
|
|
31
|
+
'utm-source': 'utm_sources',
|
|
32
|
+
'utm-campaign': 'utm_campaigns',
|
|
33
|
+
'utm-content': 'utm_contents',
|
|
34
|
+
'utm-term': 'utm_terms'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Accepts underscore/hyphen and singular/plural variants
|
|
38
|
+
const SEGMENT_NORMALIZE: Record<string, DialogSegment> = {
|
|
39
|
+
channels: 'channels',
|
|
40
|
+
sources: 'sources',
|
|
41
|
+
// mediums
|
|
42
|
+
'utm-medium': 'utm_mediums',
|
|
43
|
+
utm_medium: 'utm_mediums',
|
|
44
|
+
'utm-mediums': 'utm_mediums',
|
|
45
|
+
utm_mediums: 'utm_mediums',
|
|
46
|
+
// sources
|
|
47
|
+
'utm-source': 'utm_sources',
|
|
48
|
+
utm_source: 'utm_sources',
|
|
49
|
+
'utm-sources': 'utm_sources',
|
|
50
|
+
utm_sources: 'utm_sources',
|
|
51
|
+
// campaigns
|
|
52
|
+
'utm-campaign': 'utm_campaigns',
|
|
53
|
+
utm_campaign: 'utm_campaigns',
|
|
54
|
+
'utm-campaigns': 'utm_campaigns',
|
|
55
|
+
utm_campaigns: 'utm_campaigns',
|
|
56
|
+
// contents
|
|
57
|
+
'utm-content': 'utm_contents',
|
|
58
|
+
utm_content: 'utm_contents',
|
|
59
|
+
'utm-contents': 'utm_contents',
|
|
60
|
+
utm_contents: 'utm_contents',
|
|
61
|
+
// terms
|
|
62
|
+
'utm-term': 'utm_terms',
|
|
63
|
+
utm_term: 'utm_terms',
|
|
64
|
+
'utm-terms': 'utm_terms',
|
|
65
|
+
utm_terms: 'utm_terms',
|
|
66
|
+
// other panels
|
|
67
|
+
pages: 'pages',
|
|
68
|
+
'entry-pages': 'entry-pages',
|
|
69
|
+
entry_pages: 'entry-pages',
|
|
70
|
+
entry: 'entry-pages',
|
|
71
|
+
'exit-pages': 'exit-pages',
|
|
72
|
+
exit_pages: 'exit-pages',
|
|
73
|
+
exit: 'exit-pages',
|
|
74
|
+
// devices
|
|
75
|
+
browsers: 'browsers',
|
|
76
|
+
'operating-systems': 'operating-systems',
|
|
77
|
+
operating_systems: 'operating-systems',
|
|
78
|
+
'screen-sizes': 'screen-sizes',
|
|
79
|
+
screen_sizes: 'screen-sizes',
|
|
80
|
+
// locations
|
|
81
|
+
countries: 'countries',
|
|
82
|
+
regions: 'regions',
|
|
83
|
+
cities: 'cities',
|
|
84
|
+
devices: 'devices',
|
|
85
|
+
locations: 'locations',
|
|
86
|
+
behaviors: 'behaviors'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function dialogSegmentForMode(mode: SourcesMode): DialogSegment {
|
|
90
|
+
return MODE_TO_SEGMENT[mode] || 'sources'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ParsedDialog =
|
|
94
|
+
| { type: 'segment'; segment: DialogSegment }
|
|
95
|
+
| { type: 'referrers'; source: string }
|
|
96
|
+
| { type: 'none' }
|
|
97
|
+
|
|
98
|
+
export function parseDialogFromPath(pathname: string): ParsedDialog {
|
|
99
|
+
const base = analyticsBasePath()
|
|
100
|
+
const refPrefix = `${base}/_/referrers/`
|
|
101
|
+
if (pathname.startsWith(refPrefix)) {
|
|
102
|
+
const source = pathname.slice(refPrefix.length)
|
|
103
|
+
if (source) {
|
|
104
|
+
try {
|
|
105
|
+
return { type: 'referrers', source: decodeURIComponent(source) }
|
|
106
|
+
} catch {
|
|
107
|
+
return { type: 'referrers', source }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const segmentPrefix = `${base}/_/`
|
|
113
|
+
if (pathname.startsWith(segmentPrefix)) {
|
|
114
|
+
const raw = pathname.slice(segmentPrefix.length)
|
|
115
|
+
const seg = SEGMENT_NORMALIZE[raw]
|
|
116
|
+
if (seg) return { type: 'segment', segment: seg }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { type: 'none' }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildDialogPath(segment: DialogSegment, qs: string = ''): string {
|
|
123
|
+
const base = `${analyticsBasePath()}/_/${segment}`
|
|
124
|
+
return qs ? `${base}?${qs}` : base
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildReferrersPath(source: string, qs: string = ''): string {
|
|
128
|
+
const base = `${analyticsBasePath()}/_/referrers/${encodeURIComponent(source)}`
|
|
129
|
+
return qs ? `${base}?${qs}` : base
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function baseAnalyticsPath(qs: string = ''): string {
|
|
133
|
+
const base = analyticsBasePath()
|
|
134
|
+
return qs ? `${base}?${qs}` : base
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Map dialog segment back to the Sources panel mode
|
|
138
|
+
export function modeForSegment(segment: DialogSegment): SourcesMode | null {
|
|
139
|
+
switch (segment) {
|
|
140
|
+
case 'channels':
|
|
141
|
+
return 'channels'
|
|
142
|
+
case 'sources':
|
|
143
|
+
return 'all'
|
|
144
|
+
case 'utm_mediums':
|
|
145
|
+
return 'utm-medium'
|
|
146
|
+
case 'utm_sources':
|
|
147
|
+
return 'utm-source'
|
|
148
|
+
case 'utm_campaigns':
|
|
149
|
+
return 'utm-campaign'
|
|
150
|
+
case 'utm_contents':
|
|
151
|
+
return 'utm-content'
|
|
152
|
+
case 'utm_terms':
|
|
153
|
+
return 'utm-term'
|
|
154
|
+
default:
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Pages mapping helpers
|
|
160
|
+
export type PagesMode = 'pages' | 'entry' | 'exit'
|
|
161
|
+
|
|
162
|
+
export function pagesSegmentForMode(mode: PagesMode): DialogSegment {
|
|
163
|
+
switch (mode) {
|
|
164
|
+
case 'entry':
|
|
165
|
+
return 'entry-pages'
|
|
166
|
+
case 'exit':
|
|
167
|
+
return 'exit-pages'
|
|
168
|
+
case 'pages':
|
|
169
|
+
default:
|
|
170
|
+
return 'pages'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function pagesModeForSegment(segment: DialogSegment): PagesMode | null {
|
|
175
|
+
switch (segment) {
|
|
176
|
+
case 'pages':
|
|
177
|
+
return 'pages'
|
|
178
|
+
case 'entry-pages':
|
|
179
|
+
return 'entry'
|
|
180
|
+
case 'exit-pages':
|
|
181
|
+
return 'exit'
|
|
182
|
+
default:
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Devices mapping helpers
|
|
188
|
+
export type DevicesMode = 'browsers' | 'operating-systems' | 'screen-sizes'
|
|
189
|
+
|
|
190
|
+
export function devicesSegmentForMode(mode: DevicesMode): DialogSegment {
|
|
191
|
+
switch (mode) {
|
|
192
|
+
case 'operating-systems':
|
|
193
|
+
return 'operating-systems'
|
|
194
|
+
case 'screen-sizes':
|
|
195
|
+
return 'screen-sizes'
|
|
196
|
+
case 'browsers':
|
|
197
|
+
default:
|
|
198
|
+
return 'browsers'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function devicesModeForSegment(segment: DialogSegment): DevicesMode | null {
|
|
203
|
+
switch (segment) {
|
|
204
|
+
case 'browsers':
|
|
205
|
+
return 'browsers'
|
|
206
|
+
case 'operating-systems':
|
|
207
|
+
return 'operating-systems'
|
|
208
|
+
case 'screen-sizes':
|
|
209
|
+
return 'screen-sizes'
|
|
210
|
+
default:
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Locations mapping helpers
|
|
216
|
+
export type LocationsMode = 'map' | 'countries' | 'regions' | 'cities'
|
|
217
|
+
|
|
218
|
+
export function locationsSegmentForMode(mode: LocationsMode): DialogSegment {
|
|
219
|
+
switch (mode) {
|
|
220
|
+
case 'regions':
|
|
221
|
+
return 'regions'
|
|
222
|
+
case 'cities':
|
|
223
|
+
return 'cities'
|
|
224
|
+
case 'countries':
|
|
225
|
+
case 'map':
|
|
226
|
+
default:
|
|
227
|
+
return 'countries' // default dialog for locations
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function locationsModeForSegment(segment: DialogSegment): Exclude<LocationsMode, 'map'> | null {
|
|
232
|
+
switch (segment) {
|
|
233
|
+
case 'countries':
|
|
234
|
+
return 'countries'
|
|
235
|
+
case 'regions':
|
|
236
|
+
return 'regions'
|
|
237
|
+
case 'cities':
|
|
238
|
+
return 'cities'
|
|
239
|
+
default:
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Number formatting utilities following Plausible's exact implementation
|
|
2
|
+
// Source: plausible/assets/js/dashboard/util/number-formatter.ts
|
|
3
|
+
|
|
4
|
+
const THOUSAND = 1000
|
|
5
|
+
const HUNDRED_THOUSAND = 100000
|
|
6
|
+
const MILLION = 1000000
|
|
7
|
+
const HUNDRED_MILLION = 100000000
|
|
8
|
+
const BILLION = 1000000000
|
|
9
|
+
const HUNDRED_BILLION = 100000000000
|
|
10
|
+
const TRILLION = 1000000000000
|
|
11
|
+
|
|
12
|
+
const numberFormat = Intl.NumberFormat('en-US')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Formats numbers in short form with k/M/B suffixes.
|
|
16
|
+
* Examples: 1234 → "1.2k", 1234567 → "1.2M"
|
|
17
|
+
*/
|
|
18
|
+
export function numberShortFormatter(num: number): string {
|
|
19
|
+
if (num >= THOUSAND && num < MILLION) {
|
|
20
|
+
const thousands = num / THOUSAND
|
|
21
|
+
if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) {
|
|
22
|
+
return Math.floor(thousands) + 'k'
|
|
23
|
+
} else {
|
|
24
|
+
return Math.floor(thousands * 10) / 10 + 'k'
|
|
25
|
+
}
|
|
26
|
+
} else if (num >= MILLION && num < BILLION) {
|
|
27
|
+
const millions = num / MILLION
|
|
28
|
+
if (millions === Math.floor(millions) || num >= HUNDRED_MILLION) {
|
|
29
|
+
return Math.floor(millions) + 'M'
|
|
30
|
+
} else {
|
|
31
|
+
return Math.floor(millions * 10) / 10 + 'M'
|
|
32
|
+
}
|
|
33
|
+
} else if (num >= BILLION && num < TRILLION) {
|
|
34
|
+
const billions = num / BILLION
|
|
35
|
+
if (billions === Math.floor(billions) || num >= HUNDRED_BILLION) {
|
|
36
|
+
return Math.floor(billions) + 'B'
|
|
37
|
+
} else {
|
|
38
|
+
return Math.floor(billions * 10) / 10 + 'B'
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
return num.toString()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats numbers with thousand separators.
|
|
47
|
+
* Examples: 1234 → "1,234", 1234567 → "1,234,567"
|
|
48
|
+
*/
|
|
49
|
+
export function numberLongFormatter(num: number): string {
|
|
50
|
+
return numberFormat.format(num)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wraps a formatter to handle null and undefined values.
|
|
55
|
+
* Returns "-" for null or undefined, otherwise applies the formatter.
|
|
56
|
+
*/
|
|
57
|
+
export function nullable<T>(
|
|
58
|
+
formatter: (num: T) => string
|
|
59
|
+
): (num: T | null | undefined) => string {
|
|
60
|
+
return (num: T | null | undefined): string => {
|
|
61
|
+
if (num == null) {
|
|
62
|
+
return '-'
|
|
63
|
+
}
|
|
64
|
+
return formatter(num)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pad(num: number, size: number): string {
|
|
69
|
+
return ('000' + num).slice(size * -1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Formats duration in seconds to human-readable format.
|
|
74
|
+
* Examples: 65 → "1m 05s", 3665 → "1h 1m 5s"
|
|
75
|
+
*/
|
|
76
|
+
export function durationFormatter(duration: number): string {
|
|
77
|
+
const hours = Math.floor(duration / 60 / 60)
|
|
78
|
+
const minutes = Math.floor(duration / 60) % 60
|
|
79
|
+
const seconds = Math.floor(duration - minutes * 60 - hours * 60 * 60)
|
|
80
|
+
if (hours > 0) {
|
|
81
|
+
return `${hours}h ${minutes}m ${seconds}s`
|
|
82
|
+
} else if (minutes > 0) {
|
|
83
|
+
return `${minutes}m ${pad(seconds, 2)}s`
|
|
84
|
+
} else {
|
|
85
|
+
return `${seconds}s`
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Formats percentage values.
|
|
91
|
+
* Backend returns decimals (0.45), we convert to whole numbers (45%)
|
|
92
|
+
* Example: 0.45 → "45%", 0.123 → "12.3%"
|
|
93
|
+
*/
|
|
94
|
+
export function percentageFormatter(value: number | null): string {
|
|
95
|
+
if (value == null || Number.isNaN(value as number)) return '-'
|
|
96
|
+
const num = Number(value)
|
|
97
|
+
// Accept both decimal fractions (0.0645) and percent values (6.45)
|
|
98
|
+
const pct = num <= 1 ? (Math.round(num * 1000) / 10) : (Math.round(num * 10) / 10)
|
|
99
|
+
return `${pct}%`
|
|
100
|
+
}
|