Initial import
Change-Id: Ibe34c4c9c5313d91ffbd1a4e6a7da85adc1d8c39
diff --git a/.Rbuildignore b/.Rbuildignore
new file mode 100644
index 0000000..0c1c641
--- /dev/null
+++ b/.Rbuildignore
@@ -0,0 +1,4 @@
+^.*\.Rproj$
+^\.Rproj\.user$
+LICENSE.md
+Readme.md
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f51ef92
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.Rproj.user
+.Rhistory
+.RData
+.Ruserdata
+cache/
diff --git a/DESCRIPTION b/DESCRIPTION
new file mode 100644
index 0000000..3416e80
--- /dev/null
+++ b/DESCRIPTION
@@ -0,0 +1,34 @@
+Package: matomor
+Type: Package
+Title: API Client Library for Matomo
+Version: 0.1.0
+Authors@R:
+ c(person(given = "Marc",
+ family = "Kupietz",
+ role = c("aut", "cre"),
+ email = "kupietz@ids-mannheim.de"))
+Description: A client package that makes the 'matomo' web service API accessible from R.
+Depends: R (>= 3.5.0)
+Language: en-US
+License: BSD_2_clause + file LICENSE
+Encoding: UTF-8
+LazyData: false
+RoxygenNote: 7.1.1
+Imports:
+ ggplot2,
+ tibble,
+ magrittr,
+ tidyr,
+ dplyr,
+ lubridate,
+ highcharter,
+ jsonlite,
+ keyring,
+ htmlwidgets,
+ utils,
+ httr,
+Suggests:
+ testthat
+Collate:
+ 'visitssummary.R'
+ 'reexports.R'
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..36f5be7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,2 @@
+YEAR: 2020
+COPYRIGHT HOLDER: Leibniz Institute for the German Language
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..357eee7
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,25 @@
+# BSD-2-Clause License
+
+Copyright (c) 2020 Leibniz Institute for the German Language
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
diff --git a/NAMESPACE b/NAMESPACE
new file mode 100644
index 0000000..6dd0595
--- /dev/null
+++ b/NAMESPACE
@@ -0,0 +1,36 @@
+# Generated by roxygen2: do not edit by hand
+
+export("%>%")
+export(as_tibble)
+export(bind_cols)
+export(complete)
+export(expand_grid)
+export(getAccessToken)
+export(getVisitsSummary)
+export(group_by)
+export(mutate)
+export(n)
+export(persistAccessToken)
+export(select)
+export(summarise)
+export(year)
+import(httr)
+import(keyring)
+import(tibble)
+importFrom(dplyr,bind_cols)
+importFrom(dplyr,bind_rows)
+importFrom(dplyr,group_by)
+importFrom(dplyr,mutate)
+importFrom(dplyr,n)
+importFrom(dplyr,rowwise)
+importFrom(dplyr,select)
+importFrom(dplyr,summarise)
+importFrom(jsonlite,fromJSON)
+importFrom(lubridate,year)
+importFrom(magrittr,"%>%")
+importFrom(tibble,as_tibble)
+importFrom(tibble,rownames_to_column)
+importFrom(tidyr,complete)
+importFrom(tidyr,expand_grid)
+importFrom(utils,head)
+importFrom(utils,tail)
diff --git a/R/reexports.R b/R/reexports.R
new file mode 100644
index 0000000..4932bb4
--- /dev/null
+++ b/R/reexports.R
@@ -0,0 +1,45 @@
+
+#' Pipe operator
+#'
+#' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details.
+#'
+#' @name %>%
+#' @rdname pipe
+#' @keywords internal
+#' @export
+#' @importFrom magrittr %>%
+#' @usage lhs \%>\% rhs
+NULL
+#' @importFrom dplyr bind_cols
+#' @export
+dplyr::bind_cols
+#' @importFrom dplyr mutate
+#' @export
+dplyr::mutate
+#' @importFrom dplyr select
+#' @export
+dplyr::select
+#' @importFrom dplyr group_by
+#' @export
+dplyr::group_by
+#' @importFrom dplyr summarise
+#' @export
+dplyr::summarise
+#' @importFrom dplyr n
+#' @export
+dplyr::n
+#' @importFrom tidyr complete
+#' @export
+tidyr::complete
+#' @importFrom tidyr expand_grid
+#' @export
+tidyr::expand_grid
+#' @importFrom lubridate year
+#' @export
+lubridate::year
+#' @importFrom tibble as_tibble rownames_to_column
+#' @export
+tibble::as_tibble
+tibble::rownames_to_column
+#' @importFrom ggplot2 ggplot geom_bar geom_line
+#' @export
diff --git a/R/visitssummary.R b/R/visitssummary.R
new file mode 100644
index 0000000..648c01b
--- /dev/null
+++ b/R/visitssummary.R
@@ -0,0 +1,176 @@
+#' Get visits summary from matomo API server
+#'
+#' See matomo Reporting API Reference (\url{https://developer.matomo.org/api-reference/reporting-api}) for details.
+#'
+#' @references \url{https://developer.matomo.org/api-reference/reporting-api}
+#'
+#' @param matomoUrl base URL of your matomo instance
+#' @param siteId matomo site id or vector of site ids
+#' @param period \code{day}, \code{week}, \code{month} or \code{year}
+#' @param date date range (see \url{https://developer.matomo.org/api-reference/reporting-api})
+#' @param filter_limit defines the maximum number of rows to be returned
+#' @param accessToken API Authentication Token - you can get this in your
+#' matomo interface under Settings -> Personal -> Settings -> API Authentication Token
+#' @param removeFirst logical that determines whether the first row of each site should be removed (to account for incomplete periods)
+#' @param removeLast logical that determines whether the last row of each site should be removed (to account for incomplete periods)
+#' and pass it here, or you can make it persistent with \code{\link{persistAccessToken}}.
+#' @return data frame
+#'
+#' @import httr
+#' @importFrom jsonlite fromJSON
+#' @importFrom dplyr mutate rowwise bind_rows select summarise n
+#' @import tibble
+#' @importFrom magrittr %>%
+#' @importFrom utils head tail
+#'
+#' @examples
+#' \dontrun{
+#' df <- getVisitsSummary("https://demo.matomo.org/", siteId=3, period="day", date="last60")
+#' plot(df$nb_actions ~ df$date, type="b")
+#' }
+#'
+#' @export
+getVisitsSummary <- function(matomoUrl,
+ siteId,
+ period = "month",
+ date = "last16",
+ filter_limit = 100,
+ removeFirst = FALSE,
+ removeLast = FALSE,
+ accessToken = getAccessToken(matomoUrl)
+
+) {
+ if (is.null(accessToken) && matomoUrl != "https://demo.matomo.org/") {
+ stop(
+ paste0(
+ "You mast set an access token with persistAccessToken(\"",
+ matomoUrl,
+ "\", token) first. Get the token in your matomo interface under Settings -> Personal -> Settings -> API Authentication Token."
+ ),
+ call. = FALSE
+ )
+ }
+
+ httr::GET(
+ url = matomoUrl,
+ query = list(
+ module = "API",
+ method = "API.getBulkRequest",
+ format = "json",
+ "urls[0]" = "method=VisitsSummary.get",
+ idSite = paste0(siteId, collapse = ","),
+ date = date,
+ period = period,
+ filter_limit = filter_limit,
+ token_auth = accessToken
+ )
+ ) -> res
+
+ if (status_code(res) != 200) {
+ if (json && !http_type(res) %in% c("application/json", "application/ld+json")) {
+ stop("API did not return json", call. = FALSE)
+ }
+ result <- jsonlite::fromJSON(content(res, "text", encoding = "UTF-8"))
+ if (!is.null(result$warnings)) {
+ message <- if (nrow(result$warnings) > 1)
+ sapply(result$warnings, function(warning) paste(sprintf("%s: %s", warning[1], warning[2]), sep="\n"))
+ else
+ sprintf("%s: %s", result$warnings[1], result$warnings[2])
+ warning(message, call. = FALSE)
+ }
+ }
+
+ if (!http_type(res) %in% c("application/json", "application/ld+json")) {
+ stop("API did not return json", call. = FALSE)
+ }
+
+ json <- httr::content(res, "text", encoding = "UTF-8")
+ l <- jsonlite::fromJSON(json, simplifyVector = F)
+
+ if (!is.null(l[[1]]$result) && l[[1]]$result == "error") {
+ stop(l[[1]]$message, call. = FALSE)
+ }
+
+ (if (length(siteId) == 1) {
+ bind_rows(l[[1]], .id=period) %>%
+ head(if(removeLast) -1 else filter_limit) %>%
+ tail(if(removeFirst) -1 else filter_limit) %>%
+ mutate(site_id=siteId)
+ } else {
+ df <- bind_rows(l[[1]][[1]], .id=period) %>%
+ head(if(removeLast) -1 else filter_limit) %>%
+ tail(if(removeFirst) -1 else filter_limit) %>%
+ mutate(site_id=siteId[1])
+ for (i in 2:length(l[[1]])) {
+ df <- bind_rows(df,
+ bind_rows(l[[1]][[i]], .id=period) %>%
+ head(if(removeLast) -1 else filter_limit) %>%
+ tail(if(removeFirst) -1 else filter_limit) %>%
+ mutate(site_id=siteId[i]))
+ }
+ df
+ }) %>%
+ mutate(date = as.Date(
+ if (period == "month")
+ paste0(month, "-01")
+ else if (period == "year")
+ paste0(year, "-01-01")
+ else if (period == "week")
+ sub(",.*", "", week)
+ else if (period == "day")
+ day
+ else
+ stop(paste0("unsupported period parameter: '", period, "'"), call. = FALSE)
+ ))
+}
+
+utils::globalVariables(c("year", "month", "day", "week"))
+
+#' Save access token persistently to your keyring
+#'
+#' @param matomoUrl base URL of your matomo instance
+#' @param accessToken your oauth token
+#' @param id supply if you have multiple IDs, i.e. logins to your matomo instance
+#'
+#' @import keyring
+#'
+#' @export
+#'
+#' @examples
+#' persistAccessToken("https://demo.matomo.org/", "ad7609a669179c4ebca7c995342f7e09")
+#'
+persistAccessToken <- function(matomoUrl, accessToken, id="default") {
+ if (is.null(accessToken))
+ stop("It seems that you have not supplied any access token that could be persisted.", call. = FALSE)
+
+ keyring::key_set_with_value(matomoUrl, username=id, password=accessToken, keyring = NULL)
+}
+
+clearAccessToken <- function(matomoUrl, id="default") {
+ key_delete(matomoUrl, id)
+}
+
+
+
+#' get access token for matomo from keyring
+#'
+#' @param matomoUrl base URL of your matomo instance
+#' @param id supply if you have multiple IDs, i.e. logins to your matomo instance
+#'
+#' @return access token
+#' @export
+#'
+#' @import keyring
+#'
+getAccessToken <- function(matomoUrl, id="default") {
+ keyList <- tryCatch(withCallingHandlers(key_list(service = matomoUrl),
+ warning = function(w) invokeRestart("muffleWarning"),
+ error = function(e) return(NULL)),
+ error = function(e) { })
+ if (id %in% keyList)
+ key_get(matomoUrl, id)
+ else
+ NULL
+}
+
+
diff --git a/man/getAccessToken.Rd b/man/getAccessToken.Rd
new file mode 100644
index 0000000..b50b328
--- /dev/null
+++ b/man/getAccessToken.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/visitssummary.R
+\name{getAccessToken}
+\alias{getAccessToken}
+\title{get access token for matomo from keyring}
+\usage{
+getAccessToken(matomoUrl, id = "default")
+}
+\arguments{
+\item{matomoUrl}{base URL of your matomo instance}
+
+\item{id}{supply if you have multiple IDs, i.e. logins to your matomo instance}
+}
+\value{
+access token
+}
+\description{
+get access token for matomo from keyring
+}
diff --git a/man/getVisitsSummary.Rd b/man/getVisitsSummary.Rd
new file mode 100644
index 0000000..096897b
--- /dev/null
+++ b/man/getVisitsSummary.Rd
@@ -0,0 +1,52 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/visitssummary.R
+\name{getVisitsSummary}
+\alias{getVisitsSummary}
+\title{Get visits summary from matomo API server}
+\usage{
+getVisitsSummary(
+ matomoUrl,
+ siteId,
+ period = "month",
+ date = "last16",
+ filter_limit = 100,
+ removeFirst = FALSE,
+ removeLast = FALSE,
+ accessToken = getAccessToken(matomoUrl)
+)
+}
+\arguments{
+\item{matomoUrl}{base URL of your matomo instance}
+
+\item{siteId}{matomo site id or vector of site ids}
+
+\item{period}{\code{day}, \code{week}, \code{month} or \code{year}}
+
+\item{date}{date range (see \url{https://developer.matomo.org/api-reference/reporting-api})}
+
+\item{filter_limit}{defines the maximum number of rows to be returned}
+
+\item{removeFirst}{logical that determines whether the first row of each site should be removed (to account for incomplete periods)}
+
+\item{removeLast}{logical that determines whether the last row of each site should be removed (to account for incomplete periods)
+and pass it here, or you can make it persistent with \code{\link{persistAccessToken}}.}
+
+\item{accessToken}{API Authentication Token - you can get this in your
+matomo interface under Settings -> Personal -> Settings -> API Authentication Token}
+}
+\value{
+data frame
+}
+\description{
+See matomo Reporting API Reference (\url{https://developer.matomo.org/api-reference/reporting-api}) for details.
+}
+\examples{
+\dontrun{
+df <- getVisitsSummary("https://demo.matomo.org/", siteId=3, period="day", date="last60")
+plot(df$nb_actions ~ df$date, type="b")
+}
+
+}
+\references{
+\url{https://developer.matomo.org/api-reference/reporting-api}
+}
diff --git a/man/persistAccessToken.Rd b/man/persistAccessToken.Rd
new file mode 100644
index 0000000..9d27b51
--- /dev/null
+++ b/man/persistAccessToken.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/visitssummary.R
+\name{persistAccessToken}
+\alias{persistAccessToken}
+\title{Save access token persistently to your keyring}
+\usage{
+persistAccessToken(matomoUrl, accessToken, id = "default")
+}
+\arguments{
+\item{matomoUrl}{base URL of your matomo instance}
+
+\item{accessToken}{your oauth token}
+
+\item{id}{supply if you have multiple IDs, i.e. logins to your matomo instance}
+}
+\description{
+Save access token persistently to your keyring
+}
+\examples{
+persistAccessToken("https://demo.matomo.org/", "ad7609a669179c4ebca7c995342f7e09")
+
+}
diff --git a/man/pipe.Rd b/man/pipe.Rd
new file mode 100644
index 0000000..32004dc
--- /dev/null
+++ b/man/pipe.Rd
@@ -0,0 +1,12 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reexports.R
+\name{\%>\%}
+\alias{\%>\%}
+\title{Pipe operator}
+\usage{
+lhs \%>\% rhs
+}
+\description{
+See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details.
+}
+\keyword{internal}
diff --git a/man/reexports.Rd b/man/reexports.Rd
new file mode 100644
index 0000000..23b7907
--- /dev/null
+++ b/man/reexports.Rd
@@ -0,0 +1,31 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reexports.R
+\docType{import}
+\name{reexports}
+\alias{reexports}
+\alias{bind_cols}
+\alias{mutate}
+\alias{select}
+\alias{group_by}
+\alias{summarise}
+\alias{n}
+\alias{complete}
+\alias{expand_grid}
+\alias{year}
+\alias{as_tibble}
+\title{Objects exported from other packages}
+\keyword{internal}
+\description{
+These objects are imported from other packages. Follow the links
+below to see their documentation.
+
+\describe{
+ \item{dplyr}{\code{\link[dplyr:bind]{bind_cols}}, \code{\link[dplyr]{group_by}}, \code{\link[dplyr]{mutate}}, \code{\link[dplyr:context]{n}}, \code{\link[dplyr]{select}}, \code{\link[dplyr]{summarise}}}
+
+ \item{lubridate}{\code{\link[lubridate]{year}}}
+
+ \item{tibble}{\code{\link[tibble]{as_tibble}}}
+
+ \item{tidyr}{\code{\link[tidyr]{complete}}, \code{\link[tidyr]{expand_grid}}}
+}}
+
diff --git a/matomor.Rproj b/matomor.Rproj
new file mode 100644
index 0000000..270314b
--- /dev/null
+++ b/matomor.Rproj
@@ -0,0 +1,21 @@
+Version: 1.0
+
+RestoreWorkspace: Default
+SaveWorkspace: Default
+AlwaysSaveHistory: Default
+
+EnableCodeIndexing: Yes
+UseSpacesForTab: Yes
+NumSpacesForTab: 2
+Encoding: UTF-8
+
+RnwWeave: Sweave
+LaTeX: pdfLaTeX
+
+AutoAppendNewline: Yes
+StripTrailingWhitespace: Yes
+
+BuildType: Package
+PackageUseDevtools: Yes
+PackageInstallArgs: --no-multiarch --with-keep.source
+PackageRoxygenize: rd,collate,namespace