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 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 27600 27600"><linearGradient id="a" gradientUnits="userSpaceOnUse"/><linearGradient id="b" x1="6870" x2="24704" y1="18705" y2="18705" x:href="#a"><stop offset="0" stop-color="#0c59a4"/><stop offset="1" stop-color="#114a8b"/></linearGradient><linearGradient id="c" x1="16272" x2="5133" y1="10968" y2="23102" x:href="#a"><stop offset="0" stop-color="#1b9de2"/><stop offset=".16" stop-color="#1595df"/><stop offset=".67" stop-color="#0680d7"/><stop offset="1" stop-color="#0078d4"/></linearGradient><radialGradient id="d" cx="16720" cy="18747" r="9538" x:href="#a"><stop offset=".72" stop-opacity="0"/><stop offset=".95" stop-opacity=".53"/><stop offset="1"/></radialGradient><radialGradient id="e" cx="7130" cy="19866" r="14324" gradientTransform="matrix(.14843 -.98892 .79688 .1196 -8759 25542)" x:href="#a"><stop offset=".76" stop-opacity="0"/><stop offset=".95" stop-opacity=".5"/><stop offset="1"/></radialGradient><radialGradient id="f" cx="2523" cy="4680" r="20243" gradientTransform="matrix(-.03715 .99931 -2.12836 -.07913 13579 3530)" x:href="#a"><stop offset="0" stop-color="#35c1f1"/><stop offset=".11" stop-color="#34c1ed"/><stop offset=".23" stop-color="#2fc2df"/><stop offset=".31" stop-color="#2bc3d2"/><stop offset=".67" stop-color="#36c752"/></radialGradient><radialGradient id="g" cx="24247" cy="7758" r="9734" gradientTransform="matrix(.28109 .95968 -.78353 .22949 24510 -16292)" x:href="#a"><stop offset="0" stop-color="#66eb6e"/><stop offset="1" stop-color="#66eb6e" stop-opacity="0"/></radialGradient><path id="h" d="M24105 20053a9345 9345 0 01-1053 472 10202 10202 0 01-3590 646c-4732 0-8855-3255-8855-7432 0-1175 680-2193 1643-2729-4280 180-5380 4640-5380 7253 0 7387 6810 8137 8276 8137 791 0 1984-230 2704-456l130-44a12834 12834 0 006660-5282c220-350-168-757-535-565z"/><path id="i" d="M11571 25141a7913 7913 0 01-2273-2137 8145 8145 0 01-1514-4740 8093 8093 0 013093-6395 8082 8082 0 011373-859c312-148 846-414 1554-404a3236 3236 0 012569 1297 3184 3184 0 01636 1866c0-21 2446-7960-8005-7960-4390 0-8004 4166-8004 7820 0 2319 538 4170 1212 5604a12833 12833 0 007684 6757 12795 12795 0 003908 610c1414 0 2774-233 4045-656a7575 7575 0 01-6278-803z"/><path id="j" d="M16231 15886c-80 105-330 250-330 566 0 260 170 512 472 723 1438 1003 4149 868 4156 868a5954 5954 0 003027-839 6147 6147 0 001133-850 6180 6180 0 001910-4437c26-2242-796-3732-1133-4392-2120-4141-6694-6525-11668-6525-7011 0-12703 5635-12798 12620 47-3654 3679-6605 7996-6605 350 0 2346 34 4200 1007 1634 858 2490 1894 3086 2921 618 1067 728 2415 728 2952s-271 1333-780 1990z"/><use fill="url(#b)" x:href="#h"/><use fill="url(#d)" opacity=".35" x:href="#h"/><use fill="url(#c)" x:href="#i"/><use fill="url(#e)" opacity=".4" x:href="#i"/><use fill="url(#f)" x:href="#j"/><use fill="url(#g)" x:href="#j"/></svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<circle cx="12" cy="12" r="10"/>
|
|
3
|
+
<line x1="2" y1="12" x2="22" y2="12"/>
|
|
4
|
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 51500 51500"><radialGradient id="b" cx="87.4%" cy="-12.9%" r="128%" gradientTransform="matrix(.8 0 0 1 .18 .13)"><stop offset=".13" stop-color="#ffbd4f"/><stop offset=".28" stop-color="#ff980e"/><stop offset=".47" stop-color="#ff3750"/><stop offset=".78" stop-color="#eb0878"/><stop offset=".86" stop-color="#e50080"/></radialGradient><radialGradient id="d" cx="49%" cy="40%" r="128%" gradientTransform="matrix(.82 0 0 1 .09 0)"><stop offset=".3" stop-color="#960e18"/><stop offset=".35" stop-color="#b11927" stop-opacity=".74"/><stop offset=".43" stop-color="#db293d" stop-opacity=".34"/><stop offset=".5" stop-color="#f5334b" stop-opacity=".1"/><stop offset=".53" stop-color="#ff3750" stop-opacity="0"/></radialGradient><radialGradient id="e" cx="48%" cy="-12%" r="140%"><stop offset=".13" stop-color="#fff44f"/><stop offset=".53" stop-color="#ff980e"/></radialGradient><radialGradient id="f" cx="22.76%" cy="110.11%" r="100%"><stop offset=".35" stop-color="#3a8ee6"/><stop offset=".67" stop-color="#9059ff"/><stop offset="1" stop-color="#c139e6"/></radialGradient><radialGradient id="h" cx="52%" cy="33%" r="59%" gradientTransform="scale(.9 1)"><stop offset=".21" stop-color="#9059ff" stop-opacity="0"/><stop offset=".97" stop-color="#6e008b" stop-opacity=".6"/></radialGradient><radialGradient id="i" cx="210%" cy="-100%" r="290%"><stop offset=".1" stop-color="#ffe226"/><stop offset=".79" stop-color="#ff7139"/></radialGradient><radialGradient id="j" cx="84%" cy="-41%" r="180%"><stop offset=".11" stop-color="#fff44f"/><stop offset=".46" stop-color="#ff980e"/><stop offset=".72" stop-color="#ff3647"/><stop offset=".9" stop-color="#e31587"/></radialGradient><radialGradient id="k" cx="16.1%" cy="-18.6%" r="348.8%" gradientTransform="matrix(.10453 .46743 -.99452 .04913 -.05 -.26)"><stop offset="0" stop-color="#fff44f"/><stop offset=".3" stop-color="#ff980e"/><stop offset=".57" stop-color="#ff3647"/><stop offset=".74" stop-color="#e31587"/></radialGradient><radialGradient id="l" cx="18.9%" cy="-42.5%" r="238.4%"><stop offset=".14" stop-color="#fff44f"/><stop offset=".48" stop-color="#ff980e"/><stop offset=".66" stop-color="#ff3647"/><stop offset=".9" stop-color="#e31587"/></radialGradient><radialGradient id="m" cx="159.3%" cy="-44.72%" r="313.1%"><stop offset=".09" stop-color="#fff44f"/><stop offset=".63" stop-color="#ff980e"/></radialGradient><linearGradient id="a" x1="87.25%" x2="9.4%" y1="15.5%" y2="93.1%"><stop offset=".05" stop-color="#fff44f"/><stop offset=".37" stop-color="#ff980e"/><stop offset=".53" stop-color="#ff3647"/><stop offset=".7" stop-color="#e31587"/></linearGradient><linearGradient id="n" x1="80%" x2="18%" y1="14%" y2="84%"><stop offset=".17" stop-color="#fff44f" stop-opacity=".8"/><stop offset=".6" stop-color="#fff44f" stop-opacity="0"/></linearGradient><path id="c" d="M47870 16735c-1044-2512-3160-5224-4820-6082 1352 2650 2134 5310 2433 7294 0-6 2 5 4 22l4 26c2268 6147 1032 12398-748 16218-2754 5910-9420 11967-19857 11670-11276-318-21210-8683-23064-19643-338-1728 0-2605 170-4008-207 1080-286 1394-390 3315l-2 123c0 13270 10760 24030 24032 24030 11887 0 21756-8630 23690-19963l110-927c477-4120-53-8453-1560-12075z"/><path id="g" d="M25677 21050c-40 598-2150 2660-2890 2660-6834 0-7943 4133-7943 4133 303 3480 2726 6348 5660 7865 134 70 270 130 405 193a13277 13277 0 00706 289 10674 10674 0 003127 603c11978 562 14300-14320 5655-18640 2213-385 4510 505 5794 1407-2100-3672-6025-6150-10530-6150-285 0-564 24-844 43a12025 12025 0 00-6614 2549c366 310 780 724 1650 1583 1630 1606 5813 3270 5822 3465z"/><path fill="url(#a)" d="M47870 16735c-1044-2512-3160-5224-4820-6082 1352 2650 2134 5310 2433 7294l5 40c-2718-6773-7325-9505-11088-15452l-566-920a7372 7372 0 01-265-497 4370 4370 0 01-359-950 63 63 0 00-55-65 82 82 0 00-45 0l-12 7-17 10 10-14c-6037 3536-8085 10076-8274 13350a12025 12025 0 00-6614 2548 7136 7136 0 00-622-470 11134 11134 0 01-68-5873c-2468 1124-4390 2900-5785 4470h-10c-953-1206-886-5187-832-6018-10-52-710 363-802 425a17507 17507 0 00-2349 2012 21048 21048 0 00-2244 2692l-1 3v-3a20284 20284 0 00-3225 7280l-32 160a39700 39700 0 00-237 1500l-5 52a22907 22907 0 00-390 3316l-1 120c0 13270 10760 24030 24032 24030 11887 0 21756-8630 23690-19963l110-927c477-4120-53-8453-1560-12075zM20170 35545c113 53 220 112 334 164l16 10a12620 12620 0 01-350-174zm5506-14493zm19813-3060l-3-23 4 26z"/><use fill="url(#b)" x:href="#c"/><use fill="url(#d)" x:href="#c"/><path fill="url(#e)" d="M36192 19560l150 110a13070 13070 0 00-2231-2911C26640 9290 32150 563 33080 120l10-13c-6037 3535-8085 10076-8273 13348 280-20 560-43 844-43 4505 0 8430 2477 10530 6150z"/><use fill="url(#f)" x:href="#g"/><use fill="url(#h)" x:href="#g"/><path fill="url(#i)" d="M17083 15204a24404 24404 0 01498 330 11134 11134 0 01-67-5874c-2470 1125-4390 2900-5785 4470 115-3 3600-66 5354 1074z"/><path fill="url(#j)" d="M1822 26240c1855 10960 11788 19325 23063 19644 10437 296 17104-5762 19858-11670 1780-3820 3016-10070 748-16218v-2l-4-24c-2-17-4-28-4-22l5 40c853 5566-1980 10958-6405 14604l-13 30c-8625 7023-16878 4237-18550 3097a14410 14410 0 01-350-174c-5028-2403-7105-6984-6660-10913-4245 0-5693-3580-5693-3580s3812-2718 8836-355c4653 2190 9023 355 9023 354-10-195-4192-1860-5822-3465-872-860-1285-1272-1652-1583a7136 7136 0 00-622-470 28293 28293 0 00-498-330c-1753-1140-5240-1076-5355-1073h-10c-953-1207-886-5188-832-6020-10-50-710 363-802 426a17507 17507 0 00-2349 2012 21048 21048 0 00-2244 2692l-1 3v-3a20284 20284 0 00-3225 7280c-10 52-865 3784-444 5720z"/><path fill="url(#k)" d="M34110 16760a13070 13070 0 012231 2910l360 296c5450 5020 2594 12120 2380 12626 4426-3646 7258-9038 6405-14604-2716-6774-7323-9506-11086-15453l-566-920a7372 7372 0 01-265-497 4370 4370 0 01-359-950 63 63 0 00-55-65 82 82 0 00-45 0l-12 7-17 10c-930 443-6440 9170 1030 16640z"/><path fill="url(#l)" d="M36702 19965a4743 4743 0 00-360-295l-150-110c-1283-900-3580-1792-5794-1407 8644 4322 6323 19203-5655 18640a10674 10674 0 01-3127-603 13451 13451 0 01-706-289 9064 9064 0 01-405-193l16 10c1670 1140 9924 3925 18550-3097l13-30c213-506 3068-7606-2380-12626z"/><path fill="url(#m)" d="M14844 27844s1110-4133 7943-4133c740 0 2850-2062 2890-2660s-4370 1836-9023-354c-5024-2363-8836 354-8836 354s1448 3580 5693 3580c-445 3930 1632 8510 6660 10913 113 53 218 112 334 164-2935-1517-5358-4384-5660-7865z"/><path fill="url(#n)" d="M47870 16735c-1044-2512-3160-5224-4820-6082 1352 2650 2134 5310 2433 7294l5 40c-2718-6773-7325-9505-11088-15452l-566-920a7372 7372 0 01-265-497 4370 4370 0 01-359-950 63 63 0 00-55-65 82 82 0 00-45 0l-12 7-17 10 10-14c-6037 3536-8085 10076-8274 13350 280-20 560-43 845-43 4505 0 8430 2477 10530 6148-1284-900-3580-1792-5795-1407 8644 4322 6323 19203-5655 18640a10674 10674 0 01-3127-603 13451 13451 0 01-706-289 9064 9064 0 01-405-193l17 10a14410 14410 0 01-350-174c112 53 218 112 333 164-2935-1517-5358-4384-5660-7865 0 0 1108-4133 7942-4133 740 0 2850-2062 2890-2660-10-195-4190-1860-5822-3465-870-860-1285-1272-1650-1583a7136 7136 0 00-623-470 11134 11134 0 01-67-5873c-2470 1124-4390 2900-5785 4470h-10c-953-1207-886-5187-832-6020-10-50-710 363-802 426a17507 17507 0 00-2349 2012 21048 21048 0 00-2243 2692l-1 3v-3a20284 20284 0 00-3225 7280l-32 160a39787 39787 0 00-277 1515c-2 18 2-17 0 0a27956 27956 0 00-355 3353l-3 122c0 13270 10760 24030 24032 24030 11887 0 21756-8630 23690-19963l110-927c477-4120-53-8453-1560-12075zm-2384 1234l4 26v-2l-4-24z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1090 1090"><linearGradient id="a" x1="461" x2="461" y1="59" y2="1033" gradientUnits="userSpaceOnUse"><stop offset=".6" stop-color="#ff1b2d"/><stop offset="1" stop-color="#a70014"/></linearGradient><linearGradient id="b" x1="714" x2="714" y1="116" y2="978" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9c0000"/><stop offset=".7" stop-color="#ff4b4b"/></linearGradient><path fill="url(#a)" d="M545 42.5a502.5 502.5 0 10334.9 877.1 362.4 362.4 0 01-201.4 61.5c-119.7 0-226.8-59.4-299-153-55.6-65.6-91.5-162.5-94-271.3V533c2.5-108.8 38.4-205.8 94-271.3 72-93.6 179.3-153 299-153 73.6 0 142.5 22.5 201.4 61.6a500.8 500.8 0 00-333-127.9h-2z"/><path fill="url(#b)" d="M379.6 261.8c46-54.4 105.7-87.3 170.7-87.3 146.3 0 265 166 265 370.4 0 204.6-118.6 370.4-265 370.4-65 0-124.6-32.8-170.7-87.2 72 93.6 179.2 153 299 153A363 363 0 00880 919.6 501 501 0 001047.5 545a501.1 501.1 0 00-167.6-374.6 362.4 362.4 0 00-201.4-61.5c-119.7 0-226.8 59.4-299 153"/></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 525 525"><path fill="#7882ff" d="M203 0h120c111 0 202 90 202 203v120c0 111-90 202-203 202H202C92 525 0 435 0 322V202C0 92 90 0 203 0z"/><path fill="#4d5cc1" d="M402 263l-1-17-5-10c33 30 50 62 40 87-8 23-36 36-74 38-37 2-83-6-130-24a323 323 0 01-113-69c-27-27-39-55-30-78 9-24 40-36 81-37h1a142 142 0 00-42 55c9 25 44 55 113 84 56 23 122 35 151 16h2c4-15 7-30 7-45z"/><path fill="#fff" d="M401 246a143 143 0 01-6 61c-29 20-96 9-153-15-69-30-104-59-112-84a142 142 0 0141-55l1-1a142 142 0 01229 94zM261 404a142 142 0 01-142-136 344 344 0 00243 93 141 141 0 01-102 43z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 36600 36600"><linearGradient id="a" x1="18300" x2="18300" y1="36551" y2="50" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff780b"/><stop offset="1" stop-color="#ffa322"/></linearGradient><path fill="url(#a)" d="M28539 36550H8061A8011 8011 0 0150 28539V8061A8011 8011 0 018061 50h20479a8011 8011 0 018011 8011v20479a8011 8011 0 01-8012 8010z"/><path d="M14177 31857l-134-40c-3768-1063-5541-5252-4515-8650 539-1785 1698-3109 3211-4140 1016-694 2071-1331 3087-2023 1454-993 2554-2278 3100-3977 669-2078 448-4026-1038-5690-1562-1750-3575-2432-5886-2134-2964 382-5181 1950-6789 4427-66 101-114 212-170 319 667-431 1358-708 2128-801 1560-190 3060 843 3354 2341 226 1152-73 2206-759 3118-732 973-1537 1894-2334 2818-1837 2133-2782 4553-2387 7394 332 2386 1552 4251 3533 5587 1679 1131 3602 1460 5599 1451m5012-15353c1080-10 2135 197 3175 496a16676 16676 0 014187 1883c1241 771 2573 990 3994 753 279-46 554-137 815-243 92-38 195-178 201-277 21-342 24-687-6-1028a6592 6592 0 00-1129-3188c-798-1166-1916-1904-3239-2347-224-75-309-189-355-400-64-297-145-594-249-878-201-545-472-1050-925-1473-84 103-156 182-217 269-630 904-877 1925-878 3008 0 310-89 537-338 701-689 453-1354 953-2080 1337-960 510-1969 930-2956 1387m3759 13112a8349 8349 0 00483-2834c-8-1253-234-2465-864-3540-1426-2435-3570-3713-6418-3693-1409 10-2662 492-3687 1489a5046 5046 0 00-1178 1776c2679-2832 7031-1981 8688 571 1006 1550 1261 3208 663 4958-602 1764-1900 2855-3609 3521 3188-9 6362-52 9610-347-790-1563-2196-1785-3688-1901"/><path d="M31689 22976c-32-580-319-968-753-1258-533-355-1146-445-1768-502-1129-102-2214-369-3211-921-852-472-1669-1012-2543-1438-1375-670-2851-1003-4374-1141-1044-96-2062-10-3059 314-117 38-226 100-339 151 131 49 240 51 347 49 1174-27 2319 136 3418 554 2282 866 3938 2447 5115 4554 112 201 247 348 459 369 534 51 1074 123 1607 92 686-39 1302-353 1920-640 726-337 1489-507 2295-375 278 42 553 117 886 192m-17512 8881c-1998 9-3921-320-5600-1454-1981-1336-3201-3201-3533-5587-395-2841 550-5261 2387-7394 796-923 1601-1844 2334-2818 686-911 985-1966 759-3118-294-1498-1795-2531-3354-2341-770 94-1461 371-2128 801 56-106 106-218 170-319 1608-2477 3824-4045 6788-4427 2312-298 4324 384 5886 2134 1486 1663 1707 3612 1038 5690-546 1700-1646 2986-3100 3977-1016 694-2071 1331-3087 2023-1512 1032-2672 2356-3211 4140-1027 3398 746 7587 4515 8650 47 17 92 30 136 43m5012-15353c985-457 1994-876 2955-1386 726-386 1391-885 2080-1337 249-164 338-391 338-701 1-1083 247-2104 878-3008 61-86 133-165 217-269 453 422 724 928 925 1473 105 285 185 582 249 878 46 212 131 325 355 400 1323 443 2441 1181 3239 2347a6588 6588 0 011129 3188c30 341 28 686 6 1028a384 384 0 01-201 277 3914 3914 0 01-815 243c-1421 236-2753 18-3994-753a16625 16625 0 00-4187-1883c-1037-301-2093-507-3174-497m3759 13112c1493 114 2898 337 3691 1902-3249 294-6423 338-9610 347 1708-666 3006-1757 3609-3521 597-1750 343-3408-663-4958-1657-2550-6008-3403-8688-571a5037 5037 0 011178-1776c1024-996 2278-1478 3686-1489 2848-21 4992 1258 6418 3693 630 1076 855 2287 864 3540 4 955-153 1874-485 2833"/><path d="M31689 22976l-888-196c-805-131-1568 38-2295 375-618 287-1234 601-1920 640-533 29-1073-41-1607-92-212-21-347-168-459-369-1178-2106-2832-3686-5115-4554-1099-417-2243-580-3418-554-107 2-215 0-347-49 113-51 223-113 339-151 996-324 2015-409 3059-314 1523 140 2999 471 4374 1141 874 426 1691 966 2543 1438 998 554 2082 820 3211 921 622 56 1235 147 1768 502 437 293 724 681 755 1262m-16354-286a3895 3895 0 00-3903 3878 3879 3879 0 003909 3893 3868 3868 0 003882-3886 3878 3878 0 00-3888-3885zm-26 5677c-981-4-1782-807-1785-1787-1-984 819-1797 1813-1797s1804 820 1790 1812c-12 1008-799 1775-1818 1772z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1900 1900"><path fill="#ef3939" d="M944 1830c386 0 600 0 740-140s140-354 140-740 0-600-140-740S1330 70 944 70s-600 0-740 140S64 564 64 950s0 600 140 740 354 140 740 140z"/><linearGradient id="a" x1="61.24" x2="145.33" y1="37.94" y2="183.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity=".2"/><stop offset=".79" stop-opacity=".05"/></linearGradient><path fill="url(#a)" d="M151.6 62.4A66 66 0 0030.5 78a65.57 65.57 0 006.8 50.4c.1.2.2.4.4.6l31 53.8 25.5.2c17.1 0 30.9 0 42.2-1.2 14-1.5 24.1-5 31.9-12.8 11.3-11.3 13.5-27.6 13.9-53.8l-30.6-52.8z" transform="scale(10)"/><path fill="#fff" d="M1407 484a657.9 657.9 0 00-932 0 660.9 660.9 0 000 933 657.9 657.9 0 00932 0 660.9 660.9 0 000-933zm-39 304l-326 567c-20 35-49 56-90 59-45 3-80-16-103-55L519 786c-42-73 5-162 89-166 44-2 78 18 101 57 31 52 61 105 91 158l66 114c33 55 80 85 144 89 90 5 174-60 185-156 1-7 1-14 2-18 0-31-6-57-19-82-34-68 2-143 75-160 60-13 121 31 129 91 4 27-1 52-14 75z"/></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
|
3
|
+
<line x1="8" y1="21" x2="16" y2="21"/>
|
|
4
|
+
<line x1="12" y1="17" x2="12" y2="21"/>
|
|
5
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class AssetsController < ActionController::Base
|
|
5
|
+
# Assets are static and safe to serve without CSRF checks.
|
|
6
|
+
skip_forgery_protection
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
build_root = AhoyAnalytics::Engine.root.join("app/assets/ahoy_analytics/build")
|
|
10
|
+
images_root = AhoyAnalytics::Engine.root.join("app/assets/ahoy_analytics/images")
|
|
11
|
+
requested = params[:path].to_s
|
|
12
|
+
return head :not_found if requested.blank?
|
|
13
|
+
|
|
14
|
+
# Rails routes glob without extension; add it back from the format
|
|
15
|
+
if (fmt = params[:format].presence)
|
|
16
|
+
requested = "#{requested}.#{fmt}" unless requested.end_with?(".#{fmt}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
candidate = safe_path(build_root, requested)
|
|
20
|
+
return head :not_found if candidate.nil?
|
|
21
|
+
|
|
22
|
+
if !candidate.file? && !requested.start_with?("assets/")
|
|
23
|
+
alt = safe_path(build_root, File.join("assets", requested))
|
|
24
|
+
candidate = alt if alt&.file?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if !candidate.file?
|
|
28
|
+
candidate = safe_path(images_root, requested)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return head :not_found unless candidate.file?
|
|
32
|
+
|
|
33
|
+
expires_in 1.year, public: true
|
|
34
|
+
send_file candidate, type: Rack::Mime.mime_type(candidate.extname, "application/octet-stream"), disposition: "inline"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def safe_path(root, requested)
|
|
40
|
+
path = root.join(requested).cleanpath
|
|
41
|
+
return nil unless path.to_s.start_with?(root.to_s)
|
|
42
|
+
|
|
43
|
+
path
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "set"
|
|
6
|
+
|
|
7
|
+
module AhoyAnalytics
|
|
8
|
+
# Holds shared analytics querying logic so subcontrollers stay thin.
|
|
9
|
+
# Intentionally kept in controllers per team preference (not extracted to models/services).
|
|
10
|
+
class BaseController < AhoyAnalytics::ApplicationController
|
|
11
|
+
before_action :prepare_query
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
DEFAULT_LIMIT = 100
|
|
15
|
+
MAX_LIMIT = 500
|
|
16
|
+
MAX_SEARCH_LEN = 100
|
|
17
|
+
ALLOWED_PERIODS = %w[realtime day 7d 28d 30d 91d month 6mo 12mo year all custom].freeze
|
|
18
|
+
|
|
19
|
+
# Pagination and search helpers
|
|
20
|
+
def parsed_pagination
|
|
21
|
+
lim = params[:limit].to_i
|
|
22
|
+
pg = params[:page].to_i
|
|
23
|
+
lim = DEFAULT_LIMIT if lim <= 0 || lim > MAX_LIMIT
|
|
24
|
+
pg = 1 if pg <= 0
|
|
25
|
+
[ lim, pg ]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def normalized_search
|
|
29
|
+
term = params[:search].to_s
|
|
30
|
+
s = term.strip
|
|
31
|
+
s = s[0, MAX_SEARCH_LEN]
|
|
32
|
+
s.empty? ? nil : s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse order_by using model's whitelist/normalization
|
|
36
|
+
def parsed_order_by
|
|
37
|
+
Ahoy::Visit.parsed_order_by(params[:order_by])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initial_props(query)
|
|
41
|
+
# Align initial Sources payload mode with URL-applied filters so SSR and hydration match.
|
|
42
|
+
sources_mode = query[:mode]
|
|
43
|
+
if sources_mode.blank?
|
|
44
|
+
filters = query[:filters] || {}
|
|
45
|
+
if filters["channel"].present?
|
|
46
|
+
sources_mode = "channels"
|
|
47
|
+
elsif filters["utm_medium"].present?
|
|
48
|
+
sources_mode = "utm-medium"
|
|
49
|
+
elsif filters["utm_source"].present?
|
|
50
|
+
sources_mode = "utm-source"
|
|
51
|
+
elsif filters["utm_campaign"].present?
|
|
52
|
+
sources_mode = "utm-campaign"
|
|
53
|
+
elsif filters["utm_content"].present?
|
|
54
|
+
sources_mode = "utm-content"
|
|
55
|
+
elsif filters["utm_term"].present?
|
|
56
|
+
sources_mode = "utm-term"
|
|
57
|
+
else
|
|
58
|
+
sources_mode = "all"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
{
|
|
62
|
+
site: camelize_keys(site_context),
|
|
63
|
+
user: camelize_keys(user_context),
|
|
64
|
+
query: camelize_keys(query),
|
|
65
|
+
topStats: camelize_keys(top_stats_payload(query)),
|
|
66
|
+
mainGraph: camelize_keys(main_graph_payload(query)),
|
|
67
|
+
sources: camelize_keys(sources_payload(query.merge(mode: sources_mode))),
|
|
68
|
+
pages: camelize_keys(pages_payload(query)),
|
|
69
|
+
locations: camelize_keys(locations_payload(query)),
|
|
70
|
+
devices: camelize_keys(devices_payload(query)),
|
|
71
|
+
behaviors: camelize_keys(behaviors_payload(query))
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def prepare_query
|
|
76
|
+
@query = default_query.merge(prepared_params(params))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def prepared_params(raw_params)
|
|
80
|
+
labels = parse_labels_from_params
|
|
81
|
+
filters, advanced_filters = parse_filters_from_params(raw_params)
|
|
82
|
+
# Map numeric city id -> label if available (compat with Plausible URLs)
|
|
83
|
+
if (city = filters["city"]).present? && city =~ /^\d+$/ && labels[city].present?
|
|
84
|
+
filters["city"] = labels[city]
|
|
85
|
+
labels["city"] = labels[city]
|
|
86
|
+
labels.delete(city) # drop numeric key to avoid duplicating l= entries on FE
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Accept ISO 3166-2 region codes (e.g., "US-CA"). If a label is provided via l=region,<name>,
|
|
90
|
+
# use it. Otherwise, attempt to resolve via ISO3166 subdivisions using either the code prefix
|
|
91
|
+
# (before '-') or filters["country"].
|
|
92
|
+
if (region_code = filters["region"]).present? && region_code =~ /^[A-Za-z]{2}-[A-Za-z0-9]{1,3}$/
|
|
93
|
+
if labels["region"].present?
|
|
94
|
+
filters["region"] = labels["region"]
|
|
95
|
+
else
|
|
96
|
+
cc_from_code = region_code.split("-", 2).first.upcase
|
|
97
|
+
cc = (filters["country"].presence || cc_from_code).to_s.upcase
|
|
98
|
+
if (country = ISO3166::Country.new(cc))
|
|
99
|
+
if (sub = country.subdivisions[region_code])
|
|
100
|
+
filters["region"] = sub["name"]
|
|
101
|
+
labels["region"] = sub["name"]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
per = raw_params[:period]
|
|
108
|
+
per = "day" unless ALLOWED_PERIODS.include?(per.to_s)
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
period: per,
|
|
112
|
+
comparison: (raw_params[:comparison].to_s == "off" ? nil : raw_params[:comparison]),
|
|
113
|
+
match_day_of_week: ActiveModel::Type::Boolean.new.cast(raw_params[:match_day_of_week]),
|
|
114
|
+
date: raw_params[:date],
|
|
115
|
+
from: raw_params[:from],
|
|
116
|
+
to: raw_params[:to],
|
|
117
|
+
metric: raw_params[:metric],
|
|
118
|
+
interval: raw_params[:interval],
|
|
119
|
+
mode: raw_params[:mode],
|
|
120
|
+
funnel: raw_params[:funnel],
|
|
121
|
+
dialog: raw_params[:dialog],
|
|
122
|
+
with_imported: ActiveModel::Type::Boolean.new.cast(raw_params[:with_imported]),
|
|
123
|
+
filters: filters,
|
|
124
|
+
labels: labels,
|
|
125
|
+
advanced_filters: advanced_filters
|
|
126
|
+
}.compact
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Parse filters from Plausible-style URL params: multiple f entries: f=op,dimension,clause1[,clause2]
|
|
130
|
+
# We currently map only simple equality (op == 'is') into a flat hash { dimension => clause }
|
|
131
|
+
# Labels (l=dimension,label) are URL-only; not needed server-side.
|
|
132
|
+
def parse_filters_from_params(raw_params)
|
|
133
|
+
# Collect duplicates reliably from the raw query string
|
|
134
|
+
cgi_map = CGI.parse(request.query_string.to_s)
|
|
135
|
+
list = Array(cgi_map["f"]) # e.g., ["is,country,US", "is,region,New York"]
|
|
136
|
+
# Fallback: include single :f if present in params
|
|
137
|
+
f = raw_params[:f]
|
|
138
|
+
list |= Array(f).compact if f.present?
|
|
139
|
+
return [ {}, [] ] if list.empty?
|
|
140
|
+
|
|
141
|
+
filters = {}
|
|
142
|
+
advanced = []
|
|
143
|
+
list.each do |token|
|
|
144
|
+
parts = token.to_s.split(",")
|
|
145
|
+
next if parts.length < 3
|
|
146
|
+
op = parts[0].to_s
|
|
147
|
+
dim = parts[1].to_s
|
|
148
|
+
clause = parts[2].to_s
|
|
149
|
+
next if dim.blank? || clause.blank?
|
|
150
|
+
if op == "is"
|
|
151
|
+
# Normalize Plausible-style dimensions we support plus goal
|
|
152
|
+
if dim == "event:goal" || dim == "goal"
|
|
153
|
+
filters["goal"] = clause
|
|
154
|
+
else
|
|
155
|
+
filters[dim] = clause
|
|
156
|
+
end
|
|
157
|
+
elsif [ "is_not", "contains" ].include?(op)
|
|
158
|
+
advanced << [ op, dim, clause ]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
[ filters, advanced ]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Parse labels from repeated l=dimension,label tokens in the query string.
|
|
165
|
+
# For cities Plausible uses numeric ids as clauses; the label provides the human name.
|
|
166
|
+
def parse_labels_from_params
|
|
167
|
+
cgi_map = CGI.parse(request.query_string.to_s)
|
|
168
|
+
list = Array(cgi_map["l"]) # e.g., ["country,United States", "2988507,Paris"]
|
|
169
|
+
labels = {}
|
|
170
|
+
list.each do |token|
|
|
171
|
+
key, value = token.to_s.split(",", 2)
|
|
172
|
+
next if key.blank? || value.blank?
|
|
173
|
+
labels[key] = value
|
|
174
|
+
end
|
|
175
|
+
labels
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# normalized_filter removed; implemented in Ahoy::Visit::Filters
|
|
179
|
+
|
|
180
|
+
def default_query
|
|
181
|
+
{
|
|
182
|
+
period: "day",
|
|
183
|
+
comparison: nil,
|
|
184
|
+
match_day_of_week: true,
|
|
185
|
+
filters: {},
|
|
186
|
+
labels: {},
|
|
187
|
+
with_imported: false
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def site_context
|
|
192
|
+
defaults = {
|
|
193
|
+
domain: request.host,
|
|
194
|
+
timezone: Time.zone.name,
|
|
195
|
+
has_goals: false,
|
|
196
|
+
has_props: true,
|
|
197
|
+
funnels_available: true,
|
|
198
|
+
props_available: true,
|
|
199
|
+
segments: SEGMENTS,
|
|
200
|
+
flags: {
|
|
201
|
+
dbip: true
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
override = AhoyAnalytics.config.site_context
|
|
206
|
+
return defaults unless override
|
|
207
|
+
|
|
208
|
+
value = instance_exec(&override)
|
|
209
|
+
value.is_a?(Hash) ? defaults.merge(value) : defaults
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def user_context
|
|
213
|
+
defaults = { role: "viewer", email: "unknown" }
|
|
214
|
+
override = AhoyAnalytics.config.user_context
|
|
215
|
+
return defaults unless override
|
|
216
|
+
|
|
217
|
+
value = instance_exec(&override)
|
|
218
|
+
value.is_a?(Hash) ? defaults.merge(value) : defaults
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def devices_payload(query, limit: nil, page: nil, search: nil)
|
|
222
|
+
Ahoy::Visit.devices_payload(query, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def behaviors_payload(query, limit: nil, page: nil, search: nil)
|
|
226
|
+
Ahoy::Visit.behaviors_payload(query, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def sources_payload(query, limit: nil, page: nil, search: nil)
|
|
230
|
+
Ahoy::Visit.sources_payload(query, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def pages_payload(query, limit: nil, page: nil, search: nil)
|
|
234
|
+
Ahoy::Visit.pages_payload(query, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def locations_payload(query, limit: nil, page: nil, search: nil)
|
|
238
|
+
Ahoy::Visit.locations_payload(query, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def main_graph_payload(query)
|
|
242
|
+
Ahoy::Visit.main_graph_payload(query)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def top_stats_payload(query)
|
|
246
|
+
Ahoy::Visit.top_stats_payload(query)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def search_terms_payload(query, limit:, page:, search: nil)
|
|
250
|
+
Ahoy::Visit.search_terms_payload(query, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def referrers_payload(query, source, limit: nil, page: nil, search: nil)
|
|
254
|
+
Ahoy::Visit.referrers_payload(query, source, limit: limit, page: page, search: search, order_by: parsed_order_by)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# --- Query helpers ---
|
|
258
|
+
def cache_for(key)
|
|
259
|
+
digest = Digest::SHA256.hexdigest(JSON.dump([ key, @query, Ahoy::Visit.analytics_data_version ]))
|
|
260
|
+
Rails.cache.fetch([ :analytics, action_name, digest ], expires_in: 5.minutes) { yield }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def camelize_keys(value)
|
|
264
|
+
case value
|
|
265
|
+
when Array
|
|
266
|
+
value.map { |item| camelize_keys(item) }
|
|
267
|
+
when Hash
|
|
268
|
+
value.each_with_object({}) do |(key, val), memo|
|
|
269
|
+
memo[key.to_s.camelize(:lower)] = camelize_keys(val)
|
|
270
|
+
end
|
|
271
|
+
else
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Return a standard reason when imported data is requested but unsupported
|
|
277
|
+
def skip_imported_reason
|
|
278
|
+
@query[:with_imported] ? "not_supported" : nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
SEGMENTS = [
|
|
282
|
+
{ id: "all", name: "All visitors" }
|
|
283
|
+
].freeze
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class BehaviorsController < AhoyAnalytics::BaseController
|
|
5
|
+
def index
|
|
6
|
+
limit, page = parsed_pagination
|
|
7
|
+
search = normalized_search
|
|
8
|
+
payload = cache_for([ :behaviors, @query[:mode], @query[:funnel], limit, page, search, params[:order_by] ]) do
|
|
9
|
+
behaviors_payload(@query, limit:, page:, search:)
|
|
10
|
+
end
|
|
11
|
+
render json: camelize_keys(payload)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class DevicesController < AhoyAnalytics::BaseController
|
|
5
|
+
def index
|
|
6
|
+
limit, page = parsed_pagination
|
|
7
|
+
search = normalized_search
|
|
8
|
+
payload = cache_for([ :devices, @query[:mode], limit, page, search, params[:order_by] ]) do
|
|
9
|
+
devices_payload(@query, limit:, page:, search:)
|
|
10
|
+
end
|
|
11
|
+
render json: camelize_keys(payload)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class ExportController < AhoyAnalytics::BaseController
|
|
5
|
+
def show
|
|
6
|
+
range, = Ahoy::Visit.range_and_interval_for(@query[:period], nil, @query)
|
|
7
|
+
filters = @query[:filters] || {}
|
|
8
|
+
csv = Ahoy::Visit.csv_export(range, filters)
|
|
9
|
+
send_data(csv, filename: "analytics-export.csv", type: "text/csv")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class LocationsController < AhoyAnalytics::BaseController
|
|
5
|
+
def index
|
|
6
|
+
limit, page = parsed_pagination
|
|
7
|
+
search = normalized_search
|
|
8
|
+
payload = cache_for([ :locations_v2, @query[:mode], limit, page, search, params[:order_by] ]) do
|
|
9
|
+
locations_payload(@query, limit:, page:, search:)
|
|
10
|
+
end
|
|
11
|
+
render json: camelize_keys(payload)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class MainGraphController < AhoyAnalytics::BaseController
|
|
5
|
+
def show
|
|
6
|
+
payload = cache_for([ :main_graph, @query[:metric], @query[:interval] ]) { main_graph_payload(@query) }
|
|
7
|
+
render json: camelize_keys(payload)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class PagesController < AhoyAnalytics::BaseController
|
|
5
|
+
def index
|
|
6
|
+
limit, page = parsed_pagination
|
|
7
|
+
search = normalized_search
|
|
8
|
+
payload = cache_for([ :pages, @query[:mode], limit, page, search, params[:order_by] ]) do
|
|
9
|
+
pages_payload(@query, limit:, page:, search:)
|
|
10
|
+
end
|
|
11
|
+
render json: camelize_keys(payload)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AhoyAnalytics
|
|
4
|
+
class ReferrersController < AhoyAnalytics::BaseController
|
|
5
|
+
def index
|
|
6
|
+
source = params[:source].to_s
|
|
7
|
+
return render json: camelize_keys(empty_payload), status: :ok if source.blank?
|
|
8
|
+
|
|
9
|
+
# Alias: when source is Google, return Search Terms payload (Plausible-compatible)
|
|
10
|
+
if source.casecmp("Google").zero?
|
|
11
|
+
limit, page = parsed_pagination
|
|
12
|
+
search = normalized_search
|
|
13
|
+
payload = cache_for([ :search_terms_alias, limit, page, search, params[:order_by] ]) do
|
|
14
|
+
search_terms_payload(@query, limit:, page:, search:)
|
|
15
|
+
end
|
|
16
|
+
return render json: camelize_keys(payload), status: :ok
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
limit, page = parsed_pagination
|
|
20
|
+
search = normalized_search
|
|
21
|
+
lim_opt = params[:limit].present? ? limit : nil
|
|
22
|
+
page_opt = params[:page].present? ? page : nil
|
|
23
|
+
payload = cache_for([ :referrers, source, lim_opt, page_opt, search, params[:order_by], @query[:filters], @query[:period] ]) do
|
|
24
|
+
referrers_payload(@query, source, limit: lim_opt, page: page_opt, search:)
|
|
25
|
+
end
|
|
26
|
+
render json: camelize_keys(payload)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
def empty_payload
|
|
31
|
+
{ results: [], metrics: %i[visitors bounce_rate visit_duration], meta: { has_more: false, skip_imported_reason: nil } }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|