Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GA4 engagement time and engaged session metrics #33

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 58 additions & 87 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { ComponentSettings, Manager, MCEvent } from '@managed-components/types'
import { getFinalURL } from './requestBuilder'
import { countConversion, countPageview } from './utils'

const SESSION_DURATION_IN_MIN = 30
import {
computeEngagementDuration,
countConversion,
countPageview,
sendUserEngagementEvent,
} from './utils'

const sendGaAudiences = (
event: MCEvent,
Expand Down Expand Up @@ -66,114 +69,82 @@ const sendGaAudiences = (
client.fetch(finalDoubleClickURL)
}
}
export const sendEvent = async (
eventType: string,
event: MCEvent,
settings: ComponentSettings,
manager: Manager
) => {
const { client } = event
const { finalURL, requestBody } = getFinalURL(eventType, event, settings)
console.log(
'🚀🚀🚀🚀🚀🚀🚀🚀 final URL is here and send event fires',
finalURL
)
console.log('🚀🚀🚀🚀🚀🚀🚀🚀 also manager.fetch is working: ', manager.fetch)
manager.fetch(finalURL, {
headers: { 'User-Agent': client.userAgent },
})

export default async function (manager: Manager, settings: ComponentSettings) {
const sendEvent = async (
eventType: string,
event: MCEvent,
settings: ComponentSettings
) => {
const { client } = event
const { finalURL, requestBody } = getFinalURL(eventType, event, settings)

manager.fetch(finalURL, {
headers: { 'User-Agent': client.userAgent },
})

if (settings['ga-audiences'] || event.payload['ga-audiences']) {
sendGaAudiences(event, settings, requestBody)
}

client.set('let', Date.now().toString()) // reset the last event time
if (settings['ga-audiences'] || event.payload['ga-audiences']) {
sendGaAudiences(event, settings, requestBody)
}
}

const onVisibilityChange =
(settings: ComponentSettings) => (event: MCEvent) => {
const { client, payload } = event

if (payload.visibilityChange[0].state == 'visible') {
event.client.set(
'engagementStart',
payload.visibilityChange[0].timestamp
)
} else if (payload.visibilityChange[0].state == 'hidden') {
// on pageblur
computeEngagementDuration(event)

const msSinceLastEvent = Date.now() - parseInt(client.get('let') || '0') // _let = "_lastEventTime"
if (msSinceLastEvent > 10000) {
// order matters so engagement duration is set before dispatching the hit
computeEngagementDuration(event)

sendEvent('user_engagement', event, settings)

// Reset engagementDuration after event has been dispatched so it does not accumulate
event.client.set('engagementDuration', '0')
}
}
const onVisibilityChange =
(settings: ComponentSettings, manager: Manager) => (event: MCEvent) => {
const { client, payload } = event
if (payload.visibilityChange[0].state == 'visible') {
client.set('engagementStart', payload.visibilityChange[0].timestamp)
} else if (payload.visibilityChange[0].state == 'hidden') {
// when visibilityChange status changes to hidden, fire `user_engagement` event
sendUserEngagementEvent(event, settings, manager)
}
}

const computeEngagementDuration = (event: MCEvent) => {
const now = new Date(Date.now()).getTime()
export default async function (manager: Manager, settings: ComponentSettings) {
manager.createEventListener(
'visibilityChange',
onVisibilityChange(settings, manager)
)

let engagementDuration =
parseInt(event.client.get('engagementDuration') || '0') || 0
let engagementStart =
parseInt(event.client.get('engagementStart') || '0') || now
const delaySinceLast = (now - engagementStart) / 1000 / 60
manager.addEventListener('pageview', event => {
// this line does not trigger visibilityChange after a pagview, it will start triggering events only on the fist change to hidden
event.client.attachEvent('visibilityChange')

// Last interaction occured in a previous session, reset engagementStart
if (delaySinceLast > SESSION_DURATION_IN_MIN) {
engagementStart = now
// if engagement duration is >1 send a user_engagement event before pageview, to count the time on previous page properly
const engagementDuration =
parseInt(String(event.client.get('engagementDuration')), 10) || 0
if (engagementDuration >= 1) {
sendUserEngagementEvent(event, settings, manager)
}

engagementDuration += now - engagementStart

event.client.set('engagementDuration', `${engagementDuration}`)

// engagement start gets reset on every new pageview or event
const now = new Date(Date.now()).getTime()
event.client.set('engagementStart', `${now}`)
}
// Reset engagementDuration after pageview has been dispatched so it restarts the count
event.client.set('engagementDuration', '0')
// count pageviews for 'seg' value
countPageview(event.client)

manager.createEventListener('visibilityChange', onVisibilityChange(settings))
sendEvent('page_view', event, settings, manager)
})

manager.addEventListener('event', event => {
// count conversion events for 'seg' value
countConversion(event)
// order matters so engagement duration is set before dispatching the hit
computeEngagementDuration(event)

sendEvent('event', event, settings)
computeEngagementDuration(event, settings)

// Reset engagementDuration after event has been dispatched so it does not accumulate
event.client.set('engagementDuration', '0')
})

manager.addEventListener('pageview', event => {
event.client.attachEvent('visibilityChange')

// count pageviews for 'seg' value
countPageview(event.client)
// order matters so engagement duration is set before dispatching the hit

computeEngagementDuration(event)

sendEvent('page_view', event, settings)

// Reset engagementDuration after event has been dispatched so it does not accumulate
event.client.set('engagementDuration', '0')
sendEvent('event', event, settings, manager)
})

manager.addEventListener('ecommerce', async event => {
event.payload.conversion = true // set ecommerce events as conversion events
// count conversion events for 'seg' value
countConversion(event)
// order matters so engagement duration is set before dispatching the hit
computeEngagementDuration(event)
computeEngagementDuration(event, settings)

sendEvent('ecommerce', event, settings)

// Reset engagementDuration after event has been dispatched so it does not accumulate
event.client.set('engagementDuration', '0')
sendEvent('ecommerce', event, settings, manager)
})
}
35 changes: 33 additions & 2 deletions src/requestBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentSettings, MCEvent } from '@managed-components/types'
import { Client, ComponentSettings, MCEvent } from '@managed-components/types'
import {
buildProductRequest,
EVENTS,
Expand All @@ -9,6 +9,16 @@ import { flattenKeys, getParamSafely } from './utils'

const getRandomInt = () => Math.floor(2147483647 * Math.random())

const firstPageEvent = (client: Client) => {
const countedEvent = client.get('countedEvent')
if (countedEvent) {
console.log('🥑🥑🥑🥑🥑🥑🥑 this is not the first event!')
return false
} else {
console.log('🥑🥑🥑🥑🥑🥑🥑 this is the FIRST event!')
return true
}
}
function getToolRequest(
eventType: string,
event: MCEvent,
Expand Down Expand Up @@ -108,8 +118,29 @@ function getToolRequest(
//const notTheFirstSession = parseInt(requestBody['_s'] as string) > 1
const engagementDuration =
parseInt(String(client.get('engagementDuration')), 10) || 0
if (engagementDuration) {

// include _et parameter for engagement time metrics
if (
(eventType === 'event' || eventType === 'ecommerce') &&
firstPageEvent(client)
) {
requestBody._et = engagementDuration
console.log(
'💡💡💡💡💡💡💡💡 event/ecommerce includes _et: ',
requestBody._et
)
// mark first event to avoid sending _et with upcoming events on that page
event.client.set('countedEvent', '1', { scope: 'page' })
// Reset engagementDuration after event has been dispatched so it does not accumulate
event.client.set('engagementDuration', '0')
} else if (eventType === 'user_engagement') {
requestBody._et = engagementDuration
// Reset engagementDuration after event has been dispatched so it does not accumulate
event.client.set('engagementDuration', '0')
console.log(
'🐝🐝🐝🐝🐝🐝🐝 user_engagement includes _et: ',
requestBody._et
)
}

/* Start of gclid treating */
Expand Down
42 changes: 41 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Client, MCEvent } from '@managed-components/types'
import {
Client,
ComponentSettings,
MCEvent,
Manager,
} from '@managed-components/types'
import { sendEvent } from '.'
import { Settings } from 'http2'

export const flattenKeys = (obj: { [k: string]: unknown } = {}, prefix = '') =>
Object.keys(obj).reduce((acc: { [k: string]: unknown }, k) => {
Expand Down Expand Up @@ -70,3 +77,36 @@ export const countConversion = (event: MCEvent) => {
})
}
}

export const computeEngagementDuration = (
event: MCEvent,
settings: ComponentSettings
) => {
const SESSION_DURATION_IN_MIN = settings.sessionLength || 30 // inactivity time gap between sessions (in min)

const now = new Date(Date.now()).getTime()

let engagementDuration =
parseInt(event.client.get('engagementDuration') || '0') || 0
let engagementStart =
parseInt(event.client.get('engagementStart') || '0') || now
const delaySinceLast = (now - engagementStart) / 1000 / 60

// Last interaction occured in a previous session, reset engagementStart
if (delaySinceLast > SESSION_DURATION_IN_MIN) {
engagementStart = now
}

engagementDuration += now - engagementStart
event.client.set('engagementDuration', `${engagementDuration}`)
}

export const sendUserEngagementEvent = (
event: MCEvent,
settings: Settings,
manager: Manager
) => {
computeEngagementDuration(event, settings)

sendEvent('user_engagement', event, settings, manager)
}
Loading