Methods to coerce `ganalytics` objects into `googleAnalyticsR` object
jdeboer opened this issue · 6 comments
Aim: To be able to use the ganalytics
expression language with googleAnalyticsR
. This is to take advantage of the Google Analytics Reporting API Version 4 interface made available from googleAnalyticsR
while also continuing support for ganalytics
expressions.
The following coercion functions have been tested and appear to result in mostly similar object structures with googleAnalyticsR
. I am yet to test whether those objects are then accepted by the googleAnalyticsR::google_analytics_4
function and whether that function still produces the expected outputs. If so, then the next step is to finish the definitions for these coercion functions to cover any remaining object classes.
metric_operators <- c(
"EQUAL" = "==",
"LESS_THAN" = "<",
"GREATER_THAN" = ">",
"BETWEEN" = "<>"
)
dimension_operators <- c(
"REGEXP" = "=~",
"PARTIAL" = "=@",
"EXACT" = "==",
"IN_LIST" = "[]",
"NUMERIC_LESS_THAN" = "<",
"NUMERIC_GREATER_THAN" = ">",
"NUMERIC_BETWEEN" = "<>"
)
negated_operators <- c(
"!=", "!~", "!@", ">=", "<="
)
setClass("dim_fil_ga4")
setClass("met_fil_ga4")
setClass("segmentFilterClause_ga4")
setClass("orFiltersForSegment_ga4")
setClass("segmentSequenceStep_ga4")
setClass("simpleSegment_ga4")
setClass("sequenceSegment_ga4")
setClass("segmentFilter_ga4")
setClass("segmentDef_ga4")
get_expression_details <- function(from, var_operators) {
varName <- as.character(Var(from))
names(varName) <- sub("^ga:", "", varName)
operator <- Comparator(from)
negated <- operator %in% negated_operators
if(negated) operator <- Not(operator)
operator_lookup_index <- match(as.character(operator), var_operators)
operator_name <- names(var_operators)[operator_lookup_index]
operand <- as.character(Operand(from))
expressions <- character(0)
minComparisonValue <- character(0)
maxComparisonValue <- character(0)
if(operator == "<>") {
minComparisonValue <- operand[1]
maxComparisonValue <- operand[2]
} else if(inherits(from, ".metExpr")) {
minComparisonValue <- operand
} else {
expressions <- operand
}
list(
varName = varName,
operator = operator,
operator_name = operator_name,
negated = negated,
expressions = expressions,
minComparisonValue = minComparisonValue,
maxComparisonValue = maxComparisonValue
)
}
setAs("gaDimExpr", "dim_fil_ga4", def = function(from, to) {
dim_operation <- get_expression_details(from, dimension_operators)
x <- list(
dimensionName = dim_operation$varName,
not = dim_operation$negated,
operator = dim_operation$operator_name,
expressions = as.list(as.character(Operand(from))),
caseSensitive = FALSE
)
class(x) <- "dim_fil_ga4"
x
})
setAs("gaMetExpr", "met_fil_ga4", def = function(from, to) {
met_operation <- get_expression_details(from, metric_operators)
x <- list(
metricName = met_operation$varName,
not = met_operation$negated,
operator = met_operation$operator_name,
comparisonValue = as.character(Operand(from))
)
class(x) <- "met_fil_ga4"
x
})
setAs("gaDimExpr", "segmentFilterClause_ga4", def = function(from, to) {
exp_details <- get_expression_details(from, dimension_operators)
segmentDimensionFilter <- list(
dimensionName = exp_details$varName,
operator = exp_details$operator_name,
caseSensitive = NULL,
expressions = exp_details$expressions,
minComparisonValue = exp_details$minComparisonValue,
maxComparisonValue = exp_details$maxComparisonValue
)
class(segmentDimensionFilter) <- "segmentDimFilter_ga4"
x <- list(
not = exp_details$negated,
dimensionFilter = segmentDimensionFilter,
metricFilter = NULL
)
class(x) <- "segmentFilterClause_ga4"
x
})
setAs("gaMetExpr", "segmentFilterClause_ga4", def = function(from, to) {
from <- as(from, "gaSegMetExpr")
as(from, to)
})
setAs("gaSegMetExpr", "segmentFilterClause_ga4", def = function(from, to) {
exp_details <- get_expression_details(from, metric_operators)
scope <- c("perHit" = "HIT", "perSession" = "SESSION", "perUser" = "USER")[[ScopeLevel(from)]]
segmentMetricFilter <- list(
scope = scope,
metricName = exp_details$varName,
operator = exp_details$operator_name,
comparisonValue = exp_details$minComparisonValue,
maxComparisonValue = exp_details$maxComparisonValue
)
class(segmentMetricFilter) <- "segmentMetFilter_ga4"
x <- list(
not = exp_details$negated,
dimensionFilter = NULL,
metricFilter = segmentMetricFilter
)
class(x) <- "segmentFilterClause_ga4"
x
})
setAs("orExpr", "orFiltersForSegment_ga4", def = function(from, to) {
x <- list(
segmentFilterClauses = lapply(from, as, "segmentFilterClause_ga4")
)
class(x) <- "orFiltersForSegment_ga4"
x
})
setAs("andExpr", "simpleSegment_ga4", def = function(from, to) {
x <- list(
orFiltersForSegment = lapply(from, as, "orFiltersForSegment_ga4")
)
class(x) <- "simpleSegment_ga4"
x
})
setAs("gaSegmentSequenceStep", "segmentSequenceStep_ga4", def = function(from, to) {
matchType <- if(from@immediatelyPrecedes) "IMMEDIATELY_PRECEDES" else "PRECEDES"
x <- c(
as(as(from@.Data, "andExpr"), "simpleSegment_ga4"),
list(matchType = matchType)
)
class(x) <- "segmentSequenceStep_ga4"
x
})
setAs("gaSegmentSequenceFilter", "sequenceSegment_ga4", def = function(from, to) {
segmentSequenceSteps <- lapply(from, as, "segmentSequenceStep_ga4")
x <- list(
segmentSequenceSteps = segmentSequenceSteps,
firstStepShouldMatchFirstHit = from[[1]]@immediatelyPrecedes
)
class(x) <- "sequenceSegment_ga4"
x
})
setAs("gaSegmentConditionFilter", "segmentFilter_ga4", def = function(from, to) {
x <- list(
not = IsNegated(from),
simpleSegment = as(from, "simpleSegment_ga4"),
sequenceSegment = NULL
)
class(x) <- "segmentFilter_ga4"
x
})
setAs("gaSegmentSequenceFilter", "segmentFilter_ga4", def = function(from, to) {
x <- list(
not = IsNegated(from),
simpleSegment = NULL,
sequenceSegment = as(from, "sequenceSegment_ga4")
)
class(x) <- "segmentFilter_ga4"
x
})
setAs("gaSegmentFilterList", "segmentDef_ga4", def = function(from, to) {
x <- list(
segmentFilters = lapply(from, as, "segmentFilter_ga4")
)
class(x) <- "segmentDef_ga4"
x
})
Nice, so the idea is the same expressions will work in v3 or v4 ? I see you have gone through the objects, they should all be in this file: https://github.com/MarkEdmondson1234/googleAnalyticsR/blob/master/R/ga_v4_objects.R , which essentially are just to shape the R list structure ready to be turned into JSON via jsonlite
.
What can I do to help?
Thanks @MarkEdmondson1234 . Yes, from what it seems, v3 segments (which is what ganalytics
is designed to support) can be translated into v4 segments. I think to a great extent v4 segments can also be translated back to v3, but this is limited to dimensions and operators that both versions share. For example, I don't believe v3 strictly has a 'ends with' operator and there is no explicit option for case sensitivity.
The idea is to be able to use the ganalytics
syntax for writing filters and segments for use with googleAnalyticsR
. This way ganalytics
can benefit from many of the v4 API features too. I've started to put this into practice using the coercion methods above and these are working with I've tested so far.
Here's an example R script showing how the packages can be used together with the above coercion methods put into a source file I've called "ganalytics_to_googleAnalyticsR.R
":
# Load the packages, coercion methods and authenticate with Google Analytics.
library(ganalytics)
creds <- GoogleApiCreds(appCreds = "client_secret.json")
options(googleAuthR.client_id = creds$app$key)
options(googleAuthR.client_secret = creds$app$secret)
library(googleAnalyticsR)
source("ganalytics_to_googleAnalyticsR.R")
# Identify the Google Analytics view to retrieve the data from.
view_id <- "117987738"
# Define the segments using ganalytics functions.
bounce_sessions <- Expr(~bounces != 0)
landed_on_home <- Expr(~landingPage == "/index.html")
bounced_on_home_page <- Include(bounce_sessions & landed_on_home)
didnt_bounce_on_home_page <- Exclude(bounced_on_home_page)
customers <- Include(Expr(~transactions > 0), scope = "users")
# Show how the segments would have looked in version 3 of the Google Analytics reporting API:
as(bounced_on_home_page, "character")
# [1] "sessions::condition::ga:bounces!=0;ga:landingPagePath==/index.html"
as(didnt_bounce_on_home_page, "character")
# [1] "sessions::condition::!ga:bounces!=0;ga:landingPagePath==/index.html"
as(customers, "character")
# [1] "users::condition::ga:transactions>0"
# Use the segments as version 4 segments.
google_analytics_4(
view_id, c("2017-10-01", "2017-10-31"),
metrics = c("users", "sessions", "bounces", "transactions", "transactionRevenue"),
dimensions = c("date", "segment"),
segments = list(
segment_ga4("home_bounced",
session_segment = as(bounced_on_home_page, "segmentDef_ga4")),
segment_ga4("customers",
user_segment = as(customers, "segmentDef_ga4")),
segment_ga4("customers_home_bounced",
user_segment = as(customers, "segmentDef_ga4"),
session_segment = as(bounced_on_home_page, "segmentDef_ga4")),
segment_ga4("customers_not_home_bounced",
user_segment = as(customers, "segmentDef_ga4"),
session_segment = as(didnt_bounce_on_home_page, "segmentDef_ga4"))
)
)
I think we could go a lot further with this by implementing more class coercion methods and also by extending the ganalytics grammar and vocabulary to fully support v4 API features. I'm also wondering how the coercion methods should be implemented so that users don't need to worry about how to use them? To put them under the hood so to speak. Where should they belong, and how can they be supported by googleAnalyticsR?
I've updated the ganalytics classes to cover V4 features and will add the above methods to the package to coerce to googleAnalyticsR classes. I'll also add a function to conveniently use ganalytics objects like segments and filters with googleAnalyticsR, e.g. as_gar(my_segment). Or the other idea is to define a function that wraps google_analytics_4(...) to do the translations for the user behind the scenes.
Would it be useful to export any of more of the underlying functions of googleAnalyticsR
? Another way to approach segments is very welcome .
Does the grammar cover the sequence type segments as well?
Yes, I agree, if you could export the classes defined in your package that would then allow me to import them into the ganalytics package so I can define coercion methods there. I've created a pull request (#142) for googleAnalyticsR to export the classes - in that pull request I've exported them as S4 classes, as that is what I use in ganalytics.
In the ganalytics/dev branch I've imported the googleAnalyticsR classes and have added methods to coerce from ganalytics classes to googleAnalyticsR classes.
Here's an example of a sequential segment defined with ganalytics and used by googleAnalyticsR:
library(ganalytics)
creds <- GoogleApiCreds(appCreds = "~/client_secret.json")
options(googleAuthR.client_id = creds$app$key)
options(googleAuthR.client_secret = creds$app$secret)
library(googleAnalyticsR)
view_id <- "117987738"
checkout1 <- Expr(~pagePath == "/checkout_step_1")
checkout2 <- Expr(~pagePath == "/checkout_step_2")
checkout_progression <- Include(Sequence(checkout1, checkout2))
google_analytics_4(
view_id, c("2017-10-01", "2017-10-31"),
metrics = c("users", "sessions", "bounces", "transactions", "transactionRevenue"),
dimensions = c("date", "segment"),
segments = list(
segment_ga4("checkout_progression",
session_segment = as(checkout_progression, "segmentDef_ga4"))
)
)