MarkEdmondson1234 / measurementProtocol

Use R to send server-side tracking data to Google Analytics 4. https://developers.google.com/analytics/devguides/collection/protocol/ga4
https://code.markedmondson.me/measurementProtocol/
Other
4 stars 0 forks source link

Parse out json to MPv2 events #3

Open MarkEdmondson1234 opened 2 years ago

MarkEdmondson1234 commented 2 years ago

This is helpful if say you are passing your GA4 events to Pub/Sub in GTM-ServerSide.

GA4 tag on website sends hit -> GTM-SS takes event and publishes to Pub/Sub topic -> Pub/Sub subscription of the topic pushes to Cloud Run -> Cloud Run running plumber and this library receives JSON and parses it in MP hit.

A generic mp_parse_json() is now included to help facilitate parsing any JSON structure to MP, and an mp_parse_gtm() function for the JSON structure coming from GTM.

Example JSON from GTM-SS events:

{"x-ga-protocol_version": "2", "x-ga-measurement_id": "G-43MDXK6CLZ", "x-ga-gtm_version": "2reba1", "x-ga-page_id": 1331862949, "screen_resolution": "1280x720", "language": "en-us", "client_id": "106601226.163731234", "x-ga-request_count": 1, "page_location": "https://code.markedmondson.me/shiny-cloudrun/", "page_referrer": "https://www.google.com/", "page_title": "Shiny on Google Cloud Run - Scale-to-Zero R Web Apps \u00b7 Mark Edmondson", "ga_session_id": "1637391234", "ga_session_number": 3, "x-ga-mp2-seg": "0", "event_name": "page_view", "x-ga-system_properties": {"ss": "1"}, "debug_mode": "true", "ip_override": "123.456.789.111", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "x-ga-gcs-origin": "not-specified", "user_id": "random-user"}

Some example R code using the function for GTM as outlined above:

# json will be data recieved from plumber endpoint e.g. https://endpoint.plumber-cloudrun.mypackage.net/gtm-events

# mp_parse_gtm internally uses functions demonstrated with mp_parse_json
pubsub_event <- mp_parse_gtm(demo_json)

mp_send(pubsub_event$mp_event,
        client_id = pubsub_event$user$client_id,
        user_id = pubsub_event$user$user_id,
        user_properties = pubsub_event$user$user_properties,
        connection = my_connection,
        debug_call = TRUE)

The above function uses the below functions:

demo_json <- system.file("example", "pubsub-ga4.json", package = "measurementProtocol")
demo_list <- jsonlite::fromJSON(demo_json)

# extract the event_name
name_f <- function(x) x[["event_name"]]

# extract client_id
client_id_f <- function(x) x[["client_id"]]

# extract user_id
user_id_f <- function(x) x[["user_id"]]

# simple event
mp_parse_json(demo_list,
              name_f,
              client_id_f = client_id_f,
              user_id_f = user_id_f)

# params could be assumed to be everything not a event_name of client_id
# also not allowed any starting with reserved 'ga_'
params_f <- function(x){
  x_names <- names(x)[grepl("^x-", names(x))]
  ga_names <- names(x)[grepl("^ga_", names(x))]
  x[setdiff(names(x), c("client_id","user_id" ,"event_name", x_names, ga_names))]
  }

# parse including params (could include items as well)
parsed_event <- mp_parse_json(demo_list,
                              name_f,
                              params_f = params_f,
                              client_id_f = client_id_f,
                              user_id_f = user_id_f)
parsed_event

# sending to a debug endpoint
# preferably set this in .Renviron
Sys.setenv(MP_SECRET="MY_SECRET")

# replace with your GA4 settings
my_measurement_id <- "G-1234"
my_connection <- mp_connection(my_measurement_id)
mp_send(parsed_event$mp_event,
        client_id = parsed_event$user$client_id,
        user_id = parsed_event$user$user_id,
        user_properties = parsed_event$user$user_properties,
        connection = my_connection,
        debug_call = TRUE)
MarkEdmondson1234 commented 2 years ago

A plumber script:

library(measurementProtocol)

# get mp_secret from env MP_SECRET

#* Home page
#* @get /
#* @serializer html
function() {
  "<html>
    <h1>measurementProtocol + plumber</h1>
    <p>POST to /gtm?ga_id=G-123456<p>
    <p>Debug via /gtm?ga_id=G-123456&debug=1<p>
    <p><a href=/__docs__/>Swagger docs</a>
  </html>"
}

#* Send forward a measurement protocol hit
#* @post /gtm
#* @serializer unboxedJSON
#* @parser json
function(req, ga_id, debug = 0) {

  pubsub_data <- jsonlite::fromJSON(req$postBody)

  if(is.null(pubsub_data$message) ||
     is.null(pubsub_data$message$data)){
       res$status <- 400 # bad request
       return(list(error="Pub/Sub Message Data was invalid"))
     }

  message <- pubsub_data$message

  the_data <- rawToChar(jsonlite::base64_dec(message$data))

  parsed <- suppressMessages(mp_parse_gtm(the_data))

  my_connection <- mp_connection(ga_id)

  message(
    sprintf("Sending event: %s for client.id %s",
            parsed$mp_event$name,
            parsed$user$client_id)
  )

  sent <- mp_send(parsed$mp_event,
                  client_id = parsed$user$client_id,
                  user_id = parsed$user$user_id,
                  user_properties = parsed$user$user_properties,
                  connection = my_connection,
                  debug_call = if(debug != 0) TRUE else FALSE)

  if(!isTRUE(sent)){
    res$status <- 400 # bad request
    return(list(error="MP hit failed to send"))
  }

  "OK"
}

A Dockerfile for Cloud Run

FROM gcr.io/gcer-public/measurementprotocol

COPY . ./api

ENTRYPOINT ["R", "-e", "pr <- plumber::plumb(rev(commandArgs())[1]); pr$run(host='0.0.0.0', port=as.numeric(Sys.getenv('PORT')), swagger=TRUE)"]

CMD ["/api/api.R"]

A googleCloudRunner deployment to Cloud Run

cr_deploy_plumber(
  "inst/plumber",
  remote = "measurement_protocol_proxy",
  env_vars = paste0("MP_SECRET=", Sys.getenv("MP_SECRET"))
)
MarkEdmondson1234 commented 2 years ago

It is working

image