├── .gitignore ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── examples ├── basic.rb └── event.rb ├── lib ├── plausible_api.rb └── plausible_api │ ├── api_base.rb │ ├── client.rb │ ├── configuration.rb │ ├── event │ ├── base.rb │ └── post.rb │ ├── stats │ ├── aggregate.rb │ ├── base.rb │ ├── breakdown.rb │ ├── realtime │ │ └── visitors.rb │ └── timeseries.rb │ ├── utils.rb │ └── version.rb ├── plausible_api.gemspec └── test ├── client_test.rb ├── configuration_test.rb ├── event └── post_test.rb ├── plausible_api_test.rb ├── stats ├── aggregate_test.rb ├── base_validation_test.rb ├── breakdown_test.rb ├── realtime │ └── visitors_test.rb └── timeseries_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.gem -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at luctus@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in plausible_api.gemspec 4 | gemspec 5 | 6 | gem "rake", "~> 13.1.0" 7 | gem "minitest", "~> 5.22.2" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | plausible_api (0.4.2) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | minitest (5.22.2) 10 | rake (13.1.0) 11 | 12 | PLATFORMS 13 | ruby 14 | 15 | DEPENDENCIES 16 | minitest (~> 5.22.2) 17 | plausible_api! 18 | rake (~> 13.1.0) 19 | 20 | BUNDLED WITH 21 | 2.5.6 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Gustavo Garcia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plausible API Ruby Gem 2 | This is a simple wrapper to read the Plausible API with Ruby. 3 | It's based on the WIP [API guide](https://plausible.io/docs/stats-api) 4 | 5 | ## Usage 6 | Add this gem to your Gemfile: 7 | ```rb 8 | gem 'plausible_api' 9 | ``` 10 | Then you need to initialize a Client with your `site_id` (the domain) and your `api_key`. 11 | Optionally, you can pass a third parameter in case you are using a self-hosted instance of Plausible (You don't need to add this third parameter if your are using the comercial version of Plausible). 12 | ```rb 13 | # Using the comercial version: 14 | c = PlausibleApi::Client.new("mysite.com", "MYAPIKEY") 15 | 16 | # Using a self hosted instance 17 | c = PlausibleApi::Client.new("mysite.com", "MYAPIKEY", "https://my-hosted-plausible.com") 18 | 19 | # Test if the site and token are valid 20 | c.valid? 21 | => true 22 | ``` 23 | 24 | If you will always work with the same site, you can set some (or all) of these 3 parameters 25 | before initializing the client. On a Ruby on Rails app, you can add this to an initializer like 26 | `config/initializers/plausible.rb` 27 | 28 | ```rb 29 | # Do not include a trailing slash 30 | PlausibleApi.configure do |config| 31 | config.base_url = "https://your-plausible-instance.com" 32 | config.site_id = "dailytics.com" 33 | config.api_key = "123123" 34 | end 35 | ``` 36 | 37 | And then, initializing the client simply like this: 38 | ```rb 39 | c = PlausibleApi::Client.new 40 | ``` 41 | 42 | ### Stats > Aggregate 43 | 44 | You have all these options to get the aggregate stats 45 | ```rb 46 | # Use the default parameters (3mo period + the 4 main metrics) 47 | c.aggregate 48 | 49 | # Set parameters (period, metrics, filter, compare) 50 | c.aggregate({ period: '30d' }) 51 | c.aggregate({ period: '30d', metrics: 'visitors,pageviews' }) 52 | c.aggregate({ period: '30d', metrics: 'visitors,pageviews', filters: 'event:page==/order/confirmation' }) 53 | c.aggregate({ date: '2021-01-01,2021-02-10' }) 54 | 55 | # You'll get something like this: 56 | => {"bounce_rate"=>{"value"=>81.0}, "pageviews"=>{"value"=>29}, "visit_duration"=>{"value"=>247.0}, "visitors"=>{"value"=>14}} 57 | ``` 58 | 59 | ### Stats > Timeseries 60 | 61 | You have all these options to get the timeseries 62 | ```rb 63 | # Use the default parameters (3mo period) 64 | c.timeseries 65 | 66 | # Set parameters (period, filters, interval) 67 | c.timeseries({ period: '7d' }) 68 | c.timeseries({ period: '7d', filters: 'event:page==/order/confirmation' }) 69 | c.timeseries({ date: '2021-01-01,2021-02-15' }) 70 | 71 | # You'll get something like this: 72 | => [{"date"=>"2021-01-11", "value"=>100}, {"date"=>"2021-01-12", "value"=>120}, {"date"=>"2021-01-13", "value"=>80}...] 73 | ``` 74 | 75 | ### Stats > Breakdown 76 | ```rb 77 | # Use the default parameters (30d, event:page) 78 | c.breakdown 79 | 80 | # Set parameters (property, period, metrics, limit, page, filters, date) 81 | c.breakdown({ property: 'visit:source' }) 82 | c.breakdown({ property: 'visit:source', metrics: 'visitors,pageviews' }) 83 | c.breakdown({ property: 'visit:source', metrics: 'visitors,pageviews', period: '30d' }) 84 | c.breakdown({ property: 'visit:source', metrics: 'visitors,pageviews', date: '2021-01-01,2021-02-01' }) 85 | 86 | # You'll get something like this: 87 | => [{"page"=>"/", "visitors"=>41}, {"page"=>"/plans/", "visitors"=>14}, {"page"=>"/agencies/", "visitors"=>8}, {"page"=>"/features/", "visitors"=>8}, {"page"=>"/ready/", "visitors"=>5}, {"page"=>"/contact/", "visitors"=>4}, {"page"=>"/about/", "visitors"=>3}, {"page"=>"/donate/", "visitors"=>3}] 88 | ``` 89 | 90 | ### Realtime > Visitors 91 | 92 | It's as simple as: 93 | ```rb 94 | c.realtime_visitors 95 | 96 | => 13 97 | ``` 98 | 99 | ### Events 100 | 101 | You can send an event like this: 102 | 103 | ```rb 104 | # Example using Rack::Request in Rails for user_agent and ip. 105 | c.event({ 106 | name: "signup", 107 | url: 'https://dailytics.com/users/new', 108 | user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.3", 109 | ip: "127.0.0.1" 110 | }) 111 | ``` 112 | 113 | ## Development 114 | 115 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 116 | 117 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 118 | 119 | ## Contributing 120 | 121 | Bug reports and pull requests are welcome on GitHub at https://github.com/dailytics/plausible_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/dailytics/plausible_api/blob/main/CODE_OF_CONDUCT.md). 122 | 123 | 124 | ## License 125 | 126 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 127 | 128 | ## Code of Conduct 129 | 130 | Everyone interacting in the PlausibleApi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dailytics/plausible_api/blob/main/CODE_OF_CONDUCT.md). 131 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "plausible_api" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'plausible_api' 3 | 4 | client = PlausibleApi::Client.new(ENV["SITE_ID"], ENV["TOKEN"]) 5 | p client.valid? 6 | -------------------------------------------------------------------------------- /examples/event.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'plausible_api' 3 | 4 | client = PlausibleApi::Client.new(ENV["SITE_ID"], ENV["TOKEN"]) 5 | 6 | # default all the things, pointless but works 7 | p client.event 8 | 9 | # change the name 10 | p client.event( 11 | name: "test", 12 | ) 13 | 14 | # send the whole kitchen sink 15 | p client.event( 16 | ip: "127.0.0.1", 17 | user_agent: "test", 18 | name: "test", 19 | url: "app://localhost/test", 20 | referrer: "https://example.com", 21 | revenue: {currency: "USD", amount: 1.00}, 22 | props: {foo: "bar"}, 23 | ) 24 | -------------------------------------------------------------------------------- /lib/plausible_api.rb: -------------------------------------------------------------------------------- 1 | require "plausible_api/version" 2 | require "plausible_api/client" 3 | require "plausible_api/configuration" 4 | 5 | module PlausibleApi 6 | class Error < StandardError; end 7 | 8 | class ConfigurationError < StandardError; end 9 | 10 | class << self 11 | def configuration 12 | @configuration ||= Configuration.new 13 | end 14 | 15 | def configure 16 | yield(configuration) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/plausible_api/api_base.rb: -------------------------------------------------------------------------------- 1 | module PlausibleApi 2 | class ApiBase < Utils 3 | def request_class 4 | # Net::HTTP::Post 5 | raise NotImplementedError 6 | end 7 | 8 | def request_path 9 | # "/api/event" 10 | raise NotImplementedError 11 | end 12 | 13 | def request_auth? 14 | true 15 | end 16 | 17 | def request_body 18 | nil 19 | end 20 | 21 | def request_body? 22 | present?(request_body) 23 | end 24 | 25 | def request_headers 26 | {"content-type" => "application/json"} 27 | end 28 | 29 | def parse_response(body) 30 | raise NotImplementedError 31 | end 32 | 33 | def errors 34 | raise NotImplementedError 35 | end 36 | 37 | def valid? 38 | errors.empty? 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/plausible_api/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "plausible_api/utils" 4 | require "plausible_api/api_base" 5 | require "plausible_api/stats/base" 6 | require "plausible_api/stats/realtime/visitors" 7 | require "plausible_api/stats/aggregate" 8 | require "plausible_api/stats/timeseries" 9 | require "plausible_api/stats/breakdown" 10 | require "plausible_api/event/base" 11 | require "plausible_api/event/post" 12 | 13 | require "json" 14 | require "net/http" 15 | require "uri" 16 | require "cgi" 17 | 18 | module PlausibleApi 19 | class Client < Utils 20 | 21 | attr_accessor :configuration 22 | 23 | def initialize(site_id = nil, api_key = nil, base_url = nil) 24 | @configuration = Configuration.new 25 | @configuration.api_key = presence(api_key) || PlausibleApi.configuration.api_key 26 | @configuration.site_id = presence(site_id) || PlausibleApi.configuration.site_id 27 | @configuration.base_url = presence(base_url) || PlausibleApi.configuration.base_url 28 | end 29 | 30 | def aggregate(options = {}) 31 | call PlausibleApi::Stats::Aggregate.new(options) 32 | end 33 | 34 | def timeseries(options = {}) 35 | call PlausibleApi::Stats::Timeseries.new(options) 36 | end 37 | 38 | def breakdown(options = {}) 39 | call PlausibleApi::Stats::Breakdown.new(options) 40 | end 41 | 42 | def realtime_visitors 43 | call PlausibleApi::Stats::Realtime::Visitors.new 44 | end 45 | 46 | def valid? 47 | realtime_visitors 48 | true 49 | rescue 50 | false 51 | end 52 | 53 | def event(options = {}) 54 | call PlausibleApi::Event::Post.new(options.merge(domain: configuration.site_id)) 55 | end 56 | 57 | private 58 | 59 | SUCCESS_CODES = %w[200 202].freeze 60 | 61 | def call(api) 62 | raise Error, api.errors unless api.valid? 63 | raise ConfigurationError, configuration.errors unless configuration.valid? 64 | 65 | url = "#{configuration.base_url}#{api.request_path.gsub("$SITE_ID", configuration.site_id)}" 66 | uri = URI.parse(url) 67 | 68 | req = api.request_class.new(uri.request_uri) 69 | req.initialize_http_header(api.request_headers) 70 | req.add_field("authorization", "Bearer #{configuration.api_key}") if api.request_auth? 71 | req.body = api.request_body if api.request_body? 72 | 73 | http = Net::HTTP.new(uri.host, uri.port) 74 | http.use_ssl = true 75 | 76 | response = http.request(req) 77 | 78 | if SUCCESS_CODES.include?(response.code) 79 | api.parse_response response.body 80 | else 81 | raise Error.new response 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/plausible_api/configuration.rb: -------------------------------------------------------------------------------- 1 | module PlausibleApi 2 | class Configuration 3 | attr_accessor :base_url, :default_user_agent, :api_key, :site_id 4 | 5 | # Setting up default values 6 | def initialize 7 | @base_url = "https://plausible.io" 8 | @default_user_agent = "plausible_api_ruby/#{PlausibleApi::VERSION}" 9 | @api_key = nil 10 | @site_id = nil 11 | end 12 | 13 | def valid? 14 | errors.empty? 15 | end 16 | 17 | def errors 18 | errors = [] 19 | if base_url.nil? || base_url.empty? 20 | errors.push(base_url: "base_url is required") 21 | elsif !(URI.parse base_url).is_a? URI::HTTP 22 | errors.push(base_url: "base_url is not a valid URL") 23 | elsif base_url.end_with?("/") 24 | errors.push(base_url: "base_url should not end with a trailing slash") 25 | end 26 | if api_key.nil? || api_key.empty? 27 | errors.push(api_key: "api_key is required") 28 | end 29 | if site_id.nil? || site_id.empty? 30 | errors.push(site_id: "site_id is required") 31 | end 32 | errors 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/plausible_api/event/base.rb: -------------------------------------------------------------------------------- 1 | module PlausibleApi 2 | module Event 3 | class Base < ApiBase 4 | def request_class 5 | Net::HTTP::Post 6 | end 7 | 8 | def request_path 9 | "/api/event" 10 | end 11 | 12 | def request_auth? 13 | false 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/plausible_api/event/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PlausibleApi 4 | module Event 5 | class Post < Base 6 | VALID_REVENUE_KEYS = %i[amount currency].freeze 7 | OPTIONS_IN_HEADERS = %i[ip user_agent].freeze 8 | 9 | attr_reader :domain 10 | attr_reader :ip, :user_agent, :url 11 | attr_reader :name, :props, :referrer, :revenue 12 | 13 | def initialize(options = {}) 14 | @options = options.transform_keys(&:to_sym) 15 | 16 | @domain = @options[:domain] 17 | @ip = @options[:ip] 18 | @user_agent = presence(@options[:user_agent]) || PlausibleApi.configuration.default_user_agent 19 | @name = presence(@options[:name]) || "pageview" 20 | @url = presence(@options[:url]) || "app://localhost/#{@name}" 21 | @referrer = @options[:referrer] 22 | @revenue = @options[:revenue] 23 | @props = @options[:props] 24 | end 25 | 26 | def request_body 27 | data = { 28 | url: @url, 29 | name: @name, 30 | domain: @domain 31 | } 32 | 33 | data[:props] = @props if present?(@props) 34 | data[:revenue] = @revenue if present?(@revenue) 35 | data[:referrer] = @referrer if present?(@referrer) 36 | 37 | JSON.generate(data) 38 | end 39 | 40 | def request_headers 41 | headers = { 42 | "content-type" => "application/json", 43 | "user-agent" => @user_agent 44 | } 45 | headers["x-forwarded-for"] = @ip if present?(@ip) 46 | headers 47 | end 48 | 49 | def parse_response(body) 50 | body == "ok" 51 | end 52 | 53 | def errors 54 | errors = [] 55 | errors.push(url: "url is required") if blank?(@url) 56 | errors.push(name: "name is required") if blank?(@name) 57 | errors.push(domain: "domain is required") if blank?(@domain) 58 | errors.push(user_agent: "user_agent is required") if blank?(@user_agent) 59 | 60 | if present?(@revenue) 61 | if @revenue.is_a?(Hash) 62 | unless @revenue.keys.map(&:to_sym).all? { |key| VALID_REVENUE_KEYS.include?(key) } 63 | errors.push( 64 | revenue: "revenue must have keys #{VALID_REVENUE_KEYS.join(", ")} " \ 65 | "but was #{@revenue.inspect}" 66 | ) 67 | end 68 | else 69 | errors.push(revenue: "revenue must be a Hash") 70 | end 71 | end 72 | 73 | if present?(@props) && !@props.is_a?(Hash) 74 | errors.push(props: "props must be a Hash") 75 | end 76 | 77 | errors 78 | end 79 | 80 | private 81 | 82 | def valid_revenue_keys?(revenue) 83 | revenue.keys.sort.map(&:to_sym) == VALID_REVENUE_KEYS.sort 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/plausible_api/stats/aggregate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PlausibleApi 4 | module Stats 5 | class Aggregate < Base 6 | def initialize(options = {}) 7 | super({period: "30d", 8 | metrics: "visitors,visits,pageviews,views_per_visit,bounce_rate,visit_duration,events"} 9 | .merge(options)) 10 | end 11 | 12 | def request_path_base 13 | "/api/v1/stats/aggregate?site_id=$SITE_ID" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/plausible_api/stats/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PlausibleApi 4 | module Stats 5 | class Base < ApiBase 6 | ALLOWED_PERIODS = %w[12mo 6mo month 30d 7d day custom] 7 | ALLOWED_METRICS = %w[visitors visits pageviews views_per_visit bounce_rate visit_duration events] 8 | ALLOWED_COMPARE = %w[previous_period] 9 | ALLOWED_INTERVALS = %w[date month] 10 | ALLOWED_PROPERTIES = %w[event:goal event:page visit:entry_page visit:exit_page visit:source 11 | visit:referrer visit:utm_medium visit:utm_source visit:utm_campaign 12 | visit:utm_content visit:utm_term visit:device visit:browser 13 | visit:browser_version visit:os visit:os_version visit:country visit:region visit:city 14 | event:props:.+] 15 | ALLOWED_FILTER_OPERATORS = %w[== != \|] 16 | 17 | def initialize(options = {}) 18 | @options = {compare: nil, date: nil, filters: nil, interval: nil, 19 | limit: nil, metrics: nil, page: nil, period: nil, 20 | property: nil}.merge(options) 21 | @options[:period] = "custom" if @options[:date] 22 | end 23 | 24 | def request_class 25 | Net::HTTP::Get 26 | end 27 | 28 | def request_path 29 | params = @options.select { |_, v| !v.to_s.empty? } 30 | [request_path_base, URI.encode_www_form(params)].reject { |e| e.empty? }.join("&") 31 | end 32 | 33 | def parse_response(body) 34 | JSON.parse(body)["results"] 35 | end 36 | 37 | def errors 38 | e = "Not a valid parameter. Allowed parameters are: " 39 | 40 | errors = [] 41 | if @options[:period] 42 | errors.push({period: "#{e}#{ALLOWED_PERIODS.join(", ")}"}) unless ALLOWED_PERIODS.include? @options[:period] 43 | end 44 | if @options[:metrics] 45 | metrics_array = @options[:metrics].split(",") 46 | errors.push({metrics: "#{e}#{ALLOWED_METRICS.join(", ")}"}) unless metrics_array & ALLOWED_METRICS == metrics_array 47 | end 48 | if @options[:compare] 49 | errors.push({compare: "#{e}#{ALLOWED_COMPARE.join(", ")}"}) unless ALLOWED_COMPARE.include? @options[:compare] 50 | end 51 | if @options[:interval] 52 | errors.push({interval: "#{e}#{ALLOWED_INTERVALS.join(", ")}"}) unless ALLOWED_INTERVALS.include? @options[:interval] 53 | end 54 | if @options[:property] 55 | unless @options[:property].match?(/^(#{ALLOWED_PROPERTIES.join('|')})$/) 56 | errors.push({property: "#{e}#{ALLOWED_PROPERTIES.join(", ")}"}) unless ALLOWED_PROPERTIES.include? @options[:property] 57 | end 58 | end 59 | @options[:filters]&.split(";")&.each do |filter| 60 | unless filter.match?(/^(#{ALLOWED_PROPERTIES.join('|')})(#{ALLOWED_FILTER_OPERATORS.join('|')})(.+)$/) 61 | errors.push({filters: "Filter #{filter} is not valid"}) 62 | end 63 | end 64 | 65 | if @options[:limit] 66 | errors.push({limit: "Limit param must be a positive number"}) unless @options[:limit].to_i > 0 67 | end 68 | if @options[:page] 69 | errors.push({page: "Page param must be a positive number"}) unless @options[:page].to_i > 0 70 | end 71 | if @options[:date] 72 | errors.push({date: "You must define the period parameter as custom"}) unless @options[:period] == "custom" 73 | date_array = @options[:date].split(",") 74 | errors.push({date: "You must define start and end dates divided by comma"}) unless date_array.length == 2 75 | regex = /\d{4}-\d{2}-\d{2}/ 76 | errors.push({date: "Wrong format for the start date"}) unless date_array[0]&.match?(regex) 77 | errors.push({date: "Wrong format for the end date"}) unless date_array[1]&.match?(regex) 78 | end 79 | errors 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/plausible_api/stats/breakdown.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PlausibleApi 4 | module Stats 5 | class Breakdown < Base 6 | def initialize(options = {}) 7 | super({period: "30d", property: "event:page"}.merge(options)) 8 | end 9 | 10 | def request_path_base 11 | "/api/v1/stats/breakdown?site_id=$SITE_ID" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/plausible_api/stats/realtime/visitors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PlausibleApi 4 | module Stats 5 | module Realtime 6 | class Visitors < PlausibleApi::Stats::Base 7 | def request_path_base 8 | "/api/v1/stats/realtime/visitors?site_id=$SITE_ID" 9 | end 10 | 11 | def parse_response(body) 12 | body.to_i 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/plausible_api/stats/timeseries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PlausibleApi 4 | module Stats 5 | class Timeseries < Base 6 | def initialize(options = {}) 7 | super({period: "30d"}.merge(options)) 8 | end 9 | 10 | def request_path_base 11 | "/api/v1/stats/timeseries?site_id=$SITE_ID" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/plausible_api/utils.rb: -------------------------------------------------------------------------------- 1 | module PlausibleApi 2 | class Utils 3 | def present?(value) 4 | !value.nil? && !value.empty? 5 | end 6 | 7 | def blank?(value) 8 | !present?(value) 9 | end 10 | 11 | def presence(value) 12 | return false if blank?(value) 13 | value 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/plausible_api/version.rb: -------------------------------------------------------------------------------- 1 | module PlausibleApi 2 | VERSION = "0.4.2" 3 | end 4 | -------------------------------------------------------------------------------- /plausible_api.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/plausible_api/version' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "plausible_api" 5 | spec.version = PlausibleApi::VERSION 6 | spec.authors = ["Gustavo Garcia"] 7 | spec.email = ["gustavo@dailytics.com"] 8 | 9 | spec.summary = 'A simple Plausible API wrapper for Rails' 10 | spec.description = 'A very humble wrapper for the new API by Plausible' 11 | spec.homepage = "https://github.com/dailytics/plausible_api" 12 | spec.license = "MIT" 13 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 14 | 15 | spec.metadata["homepage_uri"] = spec.homepage 16 | spec.metadata["source_code_uri"] = "https://github.com/dailytics/plausible_api" 17 | spec.metadata["changelog_uri"] = "https://github.com/dailytics/plausible_api" 18 | 19 | # Specify which files should be added to the gem when it is released. 20 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 21 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 22 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 23 | end 24 | spec.bindir = "exe" 25 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 26 | spec.require_paths = ["lib"] 27 | end 28 | -------------------------------------------------------------------------------- /test/client_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiClientTest < Minitest::Test 4 | def test_initialize_with_parameters 5 | c = PlausibleApi::Client.new("dailytics.com", "token", "https://example.com") 6 | assert_equal "dailytics.com", c.configuration.site_id 7 | assert_equal "token", c.configuration.api_key 8 | assert_equal "https://example.com", c.configuration.base_url 9 | end 10 | 11 | def test_initialize_with_parameters_and_then_defaults 12 | PlausibleApi.configure do |config| 13 | config.base_url = "https://example.com" 14 | config.api_key = "defaulttoken" 15 | config.site_id = "default.com" 16 | end 17 | first_c = PlausibleApi::Client.new("dailytics.com", "token", "https://example.com") 18 | c = PlausibleApi::Client.new 19 | assert_equal "default.com", c.configuration.site_id 20 | assert_equal "defaulttoken", c.configuration.api_key 21 | assert_equal "https://example.com", c.configuration.base_url 22 | end 23 | 24 | def test_initialize_with_configuration 25 | PlausibleApi.configure do |config| 26 | config.base_url = "https://example.com" 27 | config.api_key = "token" 28 | config.site_id = "dailytics.com" 29 | end 30 | c = PlausibleApi::Client.new 31 | assert_equal "dailytics.com", c.configuration.site_id 32 | assert_equal "token", c.configuration.api_key 33 | assert_equal "https://example.com", c.configuration.base_url 34 | end 35 | 36 | def test_raises_configuration_error 37 | assert_raises PlausibleApi::ConfigurationError do 38 | PlausibleApi.configuration.base_url = nil 39 | c = PlausibleApi::Client.new 40 | c.aggregate 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /test/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiConfigurationTest < Minitest::Test 4 | def test_default_configuration 5 | PlausibleApi.configuration.base_url = PlausibleApi::Configuration.new.base_url 6 | assert_equal PlausibleApi.configuration.base_url, "https://plausible.io" 7 | end 8 | 9 | def test_valid_configuration 10 | PlausibleApi.configuration.base_url = "https://example.com" 11 | PlausibleApi.configuration.api_key = "token" 12 | PlausibleApi.configuration.site_id = "dailytics.com" 13 | assert_equal true, PlausibleApi.configuration.valid? 14 | end 15 | 16 | def test_invalid_configuration_base_url_nil 17 | PlausibleApi.configuration.base_url = nil 18 | assert_equal false, PlausibleApi.configuration.valid? 19 | end 20 | 21 | def test_invalid_configuration_base_url_non_https 22 | PlausibleApi.configuration.base_url = "example.com" 23 | assert_equal false, PlausibleApi.configuration.valid? 24 | end 25 | 26 | def test_invalid_configuration_base_url_trailing_slash 27 | PlausibleApi.configuration.base_url = "https://example.com/" 28 | assert_equal false, PlausibleApi.configuration.valid? 29 | end 30 | 31 | def test_invalid_configuration_api_key_nil 32 | PlausibleApi.configuration.base_url = "https://example.com" 33 | PlausibleApi.configuration.site_id = "dailytics.com" 34 | PlausibleApi.configuration.api_key = nil 35 | assert_equal false, PlausibleApi.configuration.valid? 36 | end 37 | 38 | def test_invalid_configuration_site_id_nil 39 | PlausibleApi.configuration.base_url = "https://example.com" 40 | PlausibleApi.configuration.api_key = "token" 41 | PlausibleApi.configuration.site_id = nil 42 | assert_equal false, PlausibleApi.configuration.valid? 43 | end 44 | 45 | def test_configure_method 46 | PlausibleApi.configure do |config| 47 | config.base_url = "https://anotherexample.com" 48 | config.site_id = "anotherdailytics.com" 49 | config.api_key = "anothertoken" 50 | end 51 | assert_equal PlausibleApi.configuration.base_url, "https://anotherexample.com" 52 | assert_equal PlausibleApi.configuration.site_id, "anotherdailytics.com" 53 | assert_equal PlausibleApi.configuration.api_key, "anothertoken" 54 | end 55 | end -------------------------------------------------------------------------------- /test/event/post_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiEventPostTest < Minitest::Test 4 | def setup 5 | @options = { 6 | domain: "example.com" 7 | } 8 | end 9 | 10 | def test_initialize 11 | event = PlausibleApi::Event::Post.new(@options) 12 | assert_equal "pageview", event.name 13 | assert_equal "example.com", event.domain 14 | assert_equal "app://localhost/pageview", event.url 15 | assert_equal "plausible_api_ruby/#{PlausibleApi::VERSION}", event.user_agent 16 | 17 | assert_nil event.ip 18 | assert_nil event.props 19 | assert_nil event.revenue 20 | assert_nil event.referrer 21 | end 22 | 23 | def test_request_class 24 | event = PlausibleApi::Event::Post.new(@options) 25 | assert_equal Net::HTTP::Post, event.request_class 26 | end 27 | 28 | def test_request_url_base 29 | event = PlausibleApi::Event::Post.new(@options) 30 | assert_equal event.request_path, "/api/event" 31 | end 32 | 33 | def test_request_body_predicate 34 | event = PlausibleApi::Event::Post.new(@options) 35 | assert_predicate event, :request_body? 36 | end 37 | 38 | def test_request_auth_predicate 39 | event = PlausibleApi::Event::Post.new(@options) 40 | refute_predicate event, :request_auth? 41 | end 42 | 43 | def test_request_body 44 | event = PlausibleApi::Event::Post.new(@options) 45 | expected = { 46 | "name" => "pageview", 47 | "domain" => "example.com", 48 | "url" => "app://localhost/pageview" 49 | } 50 | assert_equal expected, JSON.parse(event.request_body) 51 | end 52 | 53 | def test_request_body_with_referrer 54 | @options[:referrer] = "https://johnnunemaker.com" 55 | event = PlausibleApi::Event::Post.new(@options) 56 | expected = { 57 | "name" => "pageview", 58 | "domain" => "example.com", 59 | "url" => "app://localhost/pageview", 60 | "referrer" => "https://johnnunemaker.com" 61 | } 62 | assert_equal expected, JSON.parse(event.request_body) 63 | end 64 | 65 | def test_request_body_with_revenue 66 | @options[:revenue] = {currency: "USD", amount: 1322.22} 67 | event = PlausibleApi::Event::Post.new(@options) 68 | expected = { 69 | "name" => "pageview", 70 | "domain" => "example.com", 71 | "url" => "app://localhost/pageview", 72 | "revenue" => {"currency" => "USD", "amount" => 1322.22} 73 | } 74 | assert_equal expected, JSON.parse(event.request_body) 75 | end 76 | 77 | def test_request_body_with_props 78 | @options[:props] = {test: "ing"} 79 | event = PlausibleApi::Event::Post.new(@options) 80 | expected = { 81 | "name" => "pageview", 82 | "domain" => "example.com", 83 | "url" => "app://localhost/pageview", 84 | "props" => {"test" => "ing"} 85 | } 86 | assert_equal expected, JSON.parse(event.request_body) 87 | end 88 | 89 | def test_request_headers 90 | event = PlausibleApi::Event::Post.new(@options) 91 | expected = { 92 | "content-type" => "application/json", 93 | "user-agent" => "plausible_api_ruby/#{PlausibleApi::VERSION}" 94 | } 95 | assert_equal expected, event.request_headers 96 | end 97 | 98 | def test_request_headers_with_ip 99 | @options[:ip] = "127.0.0.1" 100 | event = PlausibleApi::Event::Post.new(@options) 101 | expected = { 102 | "content-type" => "application/json", 103 | "user-agent" => "plausible_api_ruby/#{PlausibleApi::VERSION}", 104 | "x-forwarded-for" => "127.0.0.1" 105 | } 106 | assert_equal expected, event.request_headers 107 | end 108 | 109 | def test_parse_response 110 | event = PlausibleApi::Event::Post.new(@options) 111 | assert event.parse_response("ok") 112 | refute event.parse_response("blah") 113 | end 114 | 115 | def test_valid 116 | event = PlausibleApi::Event::Post.new(@options) 117 | assert_predicate event, :valid? 118 | end 119 | 120 | def test_valid_with_all_options 121 | @options.update({ 122 | user_agent: "Something Else", 123 | ip: "127.0.0.1", 124 | revenue: {currency: "USD", amount: 1322.22}, 125 | props: {test: "ing"}, 126 | referrer: "https://johnnunemaker.com" 127 | }) 128 | event = PlausibleApi::Event::Post.new(@options) 129 | assert_predicate event, :valid?, event.errors 130 | end 131 | 132 | def test_errors_domain_required 133 | @options.delete(:domain) 134 | event = PlausibleApi::Event::Post.new(@options) 135 | assert_equal [domain: "domain is required"], event.errors 136 | end 137 | 138 | def test_errors_user_agent_defaults_no_matter_what 139 | @options.delete(:user_agent) 140 | event = PlausibleApi::Event::Post.new(@options) 141 | assert_equal [], event.errors 142 | assert_equal "plausible_api_ruby/#{PlausibleApi::VERSION}", event.user_agent 143 | end 144 | 145 | def test_errors_revenue_not_hash 146 | @options[:revenue] = "foo" 147 | event = PlausibleApi::Event::Post.new(@options) 148 | assert_equal [revenue: "revenue must be a Hash"], event.errors 149 | end 150 | 151 | def test_errors_revenue_hash_with_wrong_keys 152 | @options[:revenue] = {foo: "bar"} 153 | event = PlausibleApi::Event::Post.new(@options) 154 | assert_equal [revenue: "revenue must have keys amount, currency but was {:foo=>\"bar\"}"], event.errors 155 | end 156 | 157 | def test_errors_props_not_hash 158 | @options[:props] = "foo" 159 | event = PlausibleApi::Event::Post.new(@options) 160 | assert_equal [props: "props must be a Hash"], event.errors 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/plausible_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::PlausibleApi::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/stats/aggregate_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiAggregateTest < Minitest::Test 4 | def test_default_parameters 5 | aggregate = PlausibleApi::Stats::Aggregate.new 6 | assert_equal aggregate.request_path, 7 | "/api/v1/stats/aggregate?site_id=$SITE_ID&metrics=visitors%2Cvisits%2Cpageviews%2Cviews_per_visit%2Cbounce_rate%2Cvisit_duration%2Cevents&period=30d" 8 | end 9 | 10 | def test_period_parameter 11 | aggregate = PlausibleApi::Stats::Aggregate.new({period: "7d"}) 12 | assert_equal aggregate.request_path, 13 | "/api/v1/stats/aggregate?site_id=$SITE_ID&metrics=visitors%2Cvisits%2Cpageviews%2Cviews_per_visit%2Cbounce_rate%2Cvisit_duration%2Cevents&period=7d" 14 | end 15 | 16 | def test_metrics_parameter 17 | aggregate = PlausibleApi::Stats::Aggregate.new({metrics: "visitors"}) 18 | assert_equal aggregate.request_path, 19 | "/api/v1/stats/aggregate?site_id=$SITE_ID&metrics=visitors&period=30d" 20 | end 21 | 22 | def test_filters_parameter 23 | aggregate = PlausibleApi::Stats::Aggregate.new({filters: "event:page==/order/confirmation"}) 24 | assert_equal aggregate.request_path, 25 | "/api/v1/stats/aggregate?site_id=$SITE_ID&filters=event%3Apage%3D%3D%2Forder%2Fconfirmation&metrics=visitors%2Cvisits%2Cpageviews%2Cviews_per_visit%2Cbounce_rate%2Cvisit_duration%2Cevents&period=30d" 26 | end 27 | 28 | def test_compare_parameter 29 | aggregate = PlausibleApi::Stats::Aggregate.new({compare: "previous_period"}) 30 | assert_equal aggregate.request_path, 31 | "/api/v1/stats/aggregate?site_id=$SITE_ID&compare=previous_period&metrics=visitors%2Cvisits%2Cpageviews%2Cviews_per_visit%2Cbounce_rate%2Cvisit_duration%2Cevents&period=30d" 32 | end 33 | 34 | def test_all_parameters 35 | aggregate = PlausibleApi::Stats::Aggregate.new({period: "7d", metrics: "visitors", filters: "event:page==/order/confirmation", compare: "previous_period"}) 36 | assert_equal aggregate.request_path, 37 | "/api/v1/stats/aggregate?site_id=$SITE_ID&compare=previous_period&filters=event%3Apage%3D%3D%2Forder%2Fconfirmation&metrics=visitors&period=7d" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/stats/base_validation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiBaseValidationTest < Minitest::Test 4 | def test_period_validation 5 | aggregate = PlausibleApi::Stats::Aggregate.new({period: "3d"}) 6 | assert !aggregate.valid?, aggregate.errors 7 | aggregate = PlausibleApi::Stats::Aggregate.new({period: "30d"}) 8 | assert aggregate.valid?, aggregate.errors 9 | end 10 | 11 | def test_metrics_validation 12 | aggregate = PlausibleApi::Stats::Aggregate.new({metrics: "foo"}) 13 | assert !aggregate.valid?, aggregate.errors 14 | aggregate = PlausibleApi::Stats::Aggregate.new({metrics: "pageviews"}) 15 | assert aggregate.valid?, aggregate.errors 16 | end 17 | 18 | def test_compare_validation 19 | aggregate = PlausibleApi::Stats::Aggregate.new({compare: "foo"}) 20 | assert !aggregate.valid?, aggregate.errors 21 | aggregate = PlausibleApi::Stats::Aggregate.new({compare: "previous_period"}) 22 | assert aggregate.valid?, aggregate.errors 23 | end 24 | 25 | def test_interval_validation 26 | timeseries = PlausibleApi::Stats::Timeseries.new({interval: "foo"}) 27 | assert !timeseries.valid?, timeseries.errors 28 | timeseries = PlausibleApi::Stats::Timeseries.new({interval: "month"}) 29 | assert timeseries.valid?, timeseries.errors 30 | end 31 | 32 | def test_filters_validation 33 | aggregate = PlausibleApi::Stats::Aggregate.new({filters: "foo"}) 34 | assert !aggregate.valid?, aggregate.errors 35 | aggregate = PlausibleApi::Stats::Aggregate.new({filters: "event:page"}) 36 | assert !aggregate.valid?, aggregate.errors 37 | aggregate = PlausibleApi::Stats::Aggregate.new({filters: "event:page=~/foo"}) 38 | assert !aggregate.valid?, aggregate.errors 39 | aggregate = PlausibleApi::Stats::Aggregate.new({filters: "event:page==/foo"}) 40 | assert aggregate.valid?, aggregate.errors 41 | end 42 | 43 | def test_property_validation 44 | breakdown = PlausibleApi::Stats::Breakdown.new({property: "foo"}) 45 | assert !breakdown.valid?, breakdown.errors 46 | breakdown = PlausibleApi::Stats::Breakdown.new({property: "event:page"}) 47 | assert breakdown.valid?, breakdown.errors 48 | end 49 | 50 | def test_limit_validation 51 | breakdown = PlausibleApi::Stats::Breakdown.new({limit: "foo"}) 52 | assert !breakdown.valid?, breakdown.errors 53 | breakdown = PlausibleApi::Stats::Breakdown.new({limit: 0}) 54 | assert !breakdown.valid?, breakdown.errors 55 | breakdown = PlausibleApi::Stats::Breakdown.new({limit: 1}) 56 | assert breakdown.valid?, breakdown.errors 57 | end 58 | 59 | def test_page_validation 60 | breakdown = PlausibleApi::Stats::Breakdown.new({page: "foo"}) 61 | assert !breakdown.valid?, breakdown.errors 62 | breakdown = PlausibleApi::Stats::Breakdown.new({page: 0}) 63 | assert !breakdown.valid?, breakdown.errors 64 | breakdown = PlausibleApi::Stats::Breakdown.new({page: 1}) 65 | assert breakdown.valid?, breakdown.errors 66 | end 67 | 68 | def test_date_validation 69 | breakdown = PlausibleApi::Stats::Breakdown.new({date: "foo"}) 70 | assert !breakdown.valid?, breakdown.errors 71 | breakdown = PlausibleApi::Stats::Breakdown.new({period: "custom", date: "foo"}) 72 | assert !breakdown.valid?, breakdown.errors 73 | breakdown = PlausibleApi::Stats::Breakdown.new({period: "custom", date: "foo,bar"}) 74 | assert !breakdown.valid?, breakdown.errors 75 | breakdown = PlausibleApi::Stats::Breakdown.new({period: "custom", date: "2021-01-01,bar"}) 76 | assert !breakdown.valid?, breakdown.errors 77 | breakdown = PlausibleApi::Stats::Breakdown.new({period: "custom", date: "2021-01-01,2021-01-30"}) 78 | assert breakdown.valid?, breakdown.errors 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/stats/breakdown_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiBreakdownTest < Minitest::Test 4 | def test_default_parameters 5 | breakdown = PlausibleApi::Stats::Breakdown.new 6 | assert_equal breakdown.request_path, 7 | "/api/v1/stats/breakdown?site_id=$SITE_ID&period=30d&property=event%3Apage" 8 | end 9 | 10 | def test_period_parameter 11 | breakdown = PlausibleApi::Stats::Breakdown.new({period: "7d"}) 12 | assert_equal breakdown.request_path, 13 | "/api/v1/stats/breakdown?site_id=$SITE_ID&period=7d&property=event%3Apage" 14 | end 15 | 16 | def test_property_parameter 17 | breakdown = PlausibleApi::Stats::Breakdown.new({property: "visit:source"}) 18 | assert_equal breakdown.request_path, 19 | "/api/v1/stats/breakdown?site_id=$SITE_ID&period=30d&property=visit%3Asource" 20 | end 21 | 22 | def test_filters_parameter 23 | breakdown = PlausibleApi::Stats::Breakdown.new({filters: "event:page==/order/confirmation"}) 24 | assert_equal breakdown.request_path, 25 | "/api/v1/stats/breakdown?site_id=$SITE_ID&filters=event%3Apage%3D%3D%2Forder%2Fconfirmation&period=30d&property=event%3Apage" 26 | end 27 | 28 | def test_empty_filters_parameter 29 | breakdown = PlausibleApi::Stats::Breakdown.new({filters: ""}) 30 | assert_equal breakdown.request_path, 31 | "/api/v1/stats/breakdown?site_id=$SITE_ID&period=30d&property=event%3Apage" 32 | end 33 | 34 | def test_limit_parameter 35 | breakdown = PlausibleApi::Stats::Breakdown.new({limit: 10}) 36 | assert_equal breakdown.request_path, 37 | "/api/v1/stats/breakdown?site_id=$SITE_ID&limit=10&period=30d&property=event%3Apage" 38 | end 39 | 40 | def test_page_parameter 41 | breakdown = PlausibleApi::Stats::Breakdown.new({page: 2}) 42 | assert_equal breakdown.request_path, 43 | "/api/v1/stats/breakdown?site_id=$SITE_ID&page=2&period=30d&property=event%3Apage" 44 | end 45 | 46 | def test_date_parameter 47 | breakdown = PlausibleApi::Stats::Breakdown.new({period: "custom", date: "2021-01-01,2021-01-31"}) 48 | assert_equal breakdown.request_path, 49 | "/api/v1/stats/breakdown?site_id=$SITE_ID&date=2021-01-01%2C2021-01-31&period=custom&property=event%3Apage" 50 | end 51 | 52 | def test_all_parameters 53 | breakdown = PlausibleApi::Stats::Breakdown.new({property: "visit:source", period: "7d", 54 | metrics: "pageviews", limit: 30, page:1, filters: "event:page==/order/confirmation"}) 55 | assert_equal breakdown.request_path, 56 | "/api/v1/stats/breakdown?site_id=$SITE_ID&filters=event%3Apage%3D%3D%2Forder%2Fconfirmation&limit=30&metrics=pageviews&page=1&period=7d&property=visit%3Asource" 57 | end 58 | end -------------------------------------------------------------------------------- /test/stats/realtime/visitors_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiRealtimeVisitorsTest < Minitest::Test 4 | def test_default_parameters 5 | visitors = PlausibleApi::Stats::Realtime::Visitors.new 6 | assert_equal visitors.request_path, 7 | "/api/v1/stats/realtime/visitors?site_id=$SITE_ID" 8 | end 9 | end -------------------------------------------------------------------------------- /test/stats/timeseries_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PlausibleApiTimeseriesTest < Minitest::Test 4 | def test_default_parameters 5 | timeseries = PlausibleApi::Stats::Timeseries.new 6 | assert_equal timeseries.request_path, 7 | "/api/v1/stats/timeseries?site_id=$SITE_ID&period=30d" 8 | end 9 | 10 | def test_period_parameter 11 | timeseries = PlausibleApi::Stats::Timeseries.new({period: "7d"}) 12 | assert_equal timeseries.request_path, 13 | "/api/v1/stats/timeseries?site_id=$SITE_ID&period=7d" 14 | end 15 | 16 | def test_filters_parameter 17 | timeseries = PlausibleApi::Stats::Timeseries.new({filters: "event:page==/order/confirmation"}) 18 | assert_equal timeseries.request_path, 19 | "/api/v1/stats/timeseries?site_id=$SITE_ID&filters=event%3Apage%3D%3D%2Forder%2Fconfirmation&period=30d" 20 | end 21 | 22 | def test_interval_parameter 23 | timeseries = PlausibleApi::Stats::Timeseries.new({interval: "month"}) 24 | assert_equal timeseries.request_path, 25 | "/api/v1/stats/timeseries?site_id=$SITE_ID&interval=month&period=30d" 26 | end 27 | 28 | def test_all_parameters 29 | timeseries = PlausibleApi::Stats::Timeseries.new({period: "7d", filters: "event:page==/order/confirmation", interval: "month"}) 30 | assert_equal timeseries.request_path, 31 | "/api/v1/stats/timeseries?site_id=$SITE_ID&filters=event%3Apage%3D%3D%2Forder%2Fconfirmation&interval=month&period=7d" 32 | end 33 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 2 | require "plausible_api" 3 | 4 | require "minitest/autorun" 5 | --------------------------------------------------------------------------------