Skip to content

Commit

Permalink
feat: DAH-3043 Analytics for Application Flow (#2448)
Browse files Browse the repository at this point in the history
* First pass

* Improve analytics

* fix: updates based on Tenley feedback

* PR feedback
  • Loading branch information
chadbrokaw authored Dec 19, 2024
1 parent efc32e8 commit ba927d9
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 8 deletions.
5 changes: 5 additions & 0 deletions app/assets/javascripts/account/AccountController.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ AccountController = (
$scope.passwordRegex = /^(?=.*[0-9])(?=.*[a-zA-Z])(.+){8,}$/
$scope.emailRegex = SharedService.emailRegex

$scope.logBackLinkClick = ->
path = window.location.pathname
listingId = path.split('/')[2]
AnalyticsService.trackApplicationStart(listingId || "", null, "Continue with application from save and exit")

$scope.accountForm = ->
# pick up which ever one is defined (the other will be undefined)
$scope.form.signIn ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.back-link-container.small-only-text-center ng-if="rememberedShortFormState"
a.back-link ui-sref="{{rememberedShortFormState}}"
a.back-link(ui-sref="{{rememberedShortFormState}}" ng-click="logBackLinkClick()")
span.ui-icon.ui-static.ui-medium.i-primary
svg
use xlink:href="#i-left"
Expand Down
3 changes: 3 additions & 0 deletions app/assets/javascripts/config/angularInitialize.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@
onConfirm: ->
# fires only if user clicks 'ok' to leave page
# reloads this stateChangeStart method with skipConfirm true
AnalyticsService.trackApplicationAbandon(ShortFormApplicationService.listing.listingID, null, "Leaving for " + toState.name)
$window.removeEventListener('unload', $rootScope.onUnload)

toParams.skipConfirm = true
$state.go(toState.name, toParams)
)
Expand Down
6 changes: 4 additions & 2 deletions app/assets/javascripts/config/angularRoutes.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,8 @@
]
application: [
# 'listing' is part of the params so that application waits for listing (above) to resolve
'$q', '$stateParams', '$state', 'ShortFormApplicationService', 'AccountService', 'AutosaveService', 'listing'
($q, $stateParams, $state, ShortFormApplicationService, AccountService, AutosaveService, listing) ->
'$q', '$stateParams', '$state', 'ShortFormApplicationService', 'AccountService', 'AutosaveService', 'AnalyticsService', 'listing'
($q, $stateParams, $state, ShortFormApplicationService, AccountService, AutosaveService, AnalyticsService, listing) ->
deferred = $q.defer()

# if the user just clicked the language switcher, don't reload the whole route
Expand All @@ -620,6 +620,8 @@

if ShortFormApplicationService.application.status == 'Submitted'
# send them to their review page if the application is already submitted
# user id should always be present, but we are being cautious
AnalyticsService.trackApplicationAbandon(listing.Id, AccountService.loggedInUser?.id, "Application already submitted")
$state.go('dahlia.short-form-review', {id: ShortFormApplicationService.application.id})
else if ShortFormApplicationService.application.autofill == true
$state.go('dahlia.short-form-application.autofill-preview', {id: listing.Id, lang: $stateParams.lang})
Expand Down
60 changes: 56 additions & 4 deletions app/assets/javascripts/shared/AnalyticsService.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,38 @@ AnalyticsService = ($state) ->
Service = {}
Service.timer = {}

Service.resetProperties = {
# Any properties that should be reset on each event
}

Service.trackEvent = (event, properties) ->
dataLayer = window.dataLayer || []
unless properties.label
combinedProperties = Object.assign({}, Service.resetProperties, properties)
unless combinedProperties.label
# by default, grab the end of the URL e.g. the "contact" from "/x/y/z/contact"
current_path = _.first(_.last($state.current.url.split('/')).split('?'))
properties.label = current_path
properties.event = event
dataLayer.push(properties)
combinedProperties.label = current_path
combinedProperties.event = event
combinedProperties.event_timestamp = new Date().toISOString();
dataLayer.push(combinedProperties)

# Tracks the current page as the user navigates
Service.trackCurrentPage = ->
ga = window.ga || () ->
path = Service._currentHref()
path = '/' if path == ''
ga('set', 'page', path)
ga('send', 'pageview')

# Fired when the entire application is loaded. Then events are tracked as diffs from this time as the user navigates
Service.startTimer = (opts = {}) ->
if opts.label
Service.timer[opts.label] = {start: moment()}
Service.timer[opts.label].variable = opts.variable if opts.variable

# Fired when the user clicks on the "Apply Online" button on a listing page.
# Events are tracked as diffs from the start time with specific keys as the user navigates
# Note that this is effectively deprecated as we no longer user the AngularJS listing page
Service.trackTimerEvent = (category, label, variable = '') ->
# once the timer has been cleared, we don't track it any more
return unless Service.timer[label]
Expand All @@ -40,23 +51,62 @@ AnalyticsService = ($state) ->
Service.timer[label] = null
Service.trackEvent('Timer Event', params)

Service.createApplicationTimer = (listingId) ->
currentTimeInMs = Date.now()
localStorage.setItem("Application_Analytics_#{listingId}", currentTimeInMs)

Service.getApplicationDuration = (listingId) ->
currentTimeInMs = Date.now()
startTimeInMs = localStorage.getItem("Application_Analytics_#{listingId}")

if startTimeInMs
localStorage.removeItem("Application_Analytics_#{listingId}")
return currentTimeInMs - startTimeInMs

return null

# Fired when the user creates an account, signs in, or requests a password change
# Fired when a user starts an autofilled application, continues a previous draft, or resets and starts a new application
# (This would be when the user decides not to start with an autofilled application)
# Also fired whenever a user completes any application page (No label is attached in this instance)
Service.trackFormSuccess = (category, label = null) ->
params = { category: category, action: 'Form Success', label: label }
Service.trackEvent('Form Message', params)

Service.trackApplicationStart = (listingId, userId = null, origin = null) ->
params = { user_id: userId, listing_id: listingId, application_started_origin: origin }
Service.createApplicationTimer(listingId)
Service.trackEvent('application_started', params)

Service.trackApplicationComplete = (listingId, userId = null, reason = null) ->
time_to_submit = Service.getApplicationDuration(listingId)
params = { user_id: userId, listing_id: listingId, time_to_submit: time_to_submit, reason: reason }
Service.trackEvent('application_completed', params)

Service.trackApplicationAbandon = (listingId, userId = null, reason = null) ->
time_to_abandon = Service.getApplicationDuration(listingId)
params = { user_id: userId, listing_id: listingId, time_to_abandon: time_to_abandon, reason: reason }
Service.trackEvent('application_exited', params)

# Distinct from trackFormFieldError, this function is called when there is a validation error on the form
# For example, if the user has an income that is out of bounds
Service.trackFormError = (category, label = null, opts = {}) ->
params = { category: category, action: 'Form Error', label: label }
_.merge(params, opts)
Service.trackEvent('Form Message', params)

# Fired when the user triggers a form field error, fieldId attached, error handler is global
Service.trackFormFieldError = (category, fieldId = '', opts = {}) ->
params = { category: category, action: 'Form Field Error', fieldId: fieldId }
_.merge(params, opts)
Service.trackEvent('Field Message', params)

# Fired when the user leaves the create account page without creating an account
# Fired when the user exits the application process
Service.trackFormAbandon = (category) ->
Service.trackEvent('Form Message', { category: category, action: 'Form Abandon' })

# Fired when the user inputs a lotter number that is invalid
Service.trackInvalidLotteryNumber = ->
label = Service._currentHref()
Service.trackEvent('Form Message', {
Expand All @@ -65,9 +115,11 @@ AnalyticsService = ($state) ->
label: label
})

# Fired when the user leaves an application as a result of a timeout
Service.trackTimeout = (category) ->
Service.trackEvent('Form Message', { category: category, action: 'Timeout' })

# Fired when the user reached the my account page with a confirmed account
Service.trackAccountCreation = ->
Service.trackEvent('Form Message', { category: 'Accounts', action: 'Account Creation', label: 'Account Confirmation Success' })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ ShortFormApplicationController = (

$scope.startAutofilledApp = ->
AnalyticsService.trackFormSuccess('Application', 'Start with these details')
AnalyticsService.trackApplicationStart($scope.listing.Id, AccountService.loggedInUser?.id || null, 'Start with these details')
$scope.go(ShortFormNavigationService.initialState())

$scope.trackContinuePreviousDraft = ->
AnalyticsService.trackApplicationStart($scope.listing.Id, AccountService.loggedInUser?.id || null, 'Continue with these details')
AnalyticsService.trackFormSuccess('Application', 'Continue with these details')

$scope.resetAndStartNewApp = ->
Expand All @@ -103,16 +105,24 @@ ShortFormApplicationController = (
$scope.householdMembers = ShortFormApplicationService.householdMembers
delete $scope.application.autofill
AnalyticsService.trackFormSuccess('Application', 'Reset and start from scratch')
AnalyticsService.trackApplicationStart($scope.listing.Id, AccountService.loggedInUser?.id || null, 'Reset and start from scratch')
$state.go(ShortFormNavigationService.initialState())

$scope.resetAndReplaceApp = ShortFormApplicationService.resetAndReplaceApp

$scope.onUnload = (e) ->
# We want to track the user's actual exit after confirming that they want to leave the page
# Note that this is not perfect, as this will also capture application reloads which doesn't necessarily constitute an abandonment
AnalyticsService.trackApplicationAbandon($scope.listing.Id, AccountService.loggedInUser?.id || null, "Generic Exit")


$scope.atShortFormState = ->
ShortFormApplicationService.isShortFormPage($state.current)

if $scope.atShortFormState() && !$window.jasmine && !window.protractor
# don't add this onbeforeunload inside of jasmine tests
$window.addEventListener 'beforeunload', ShortFormApplicationService.onExit
$window.addEventListener 'unload', $scope.onUnload

$scope.submitForm = ->
form = $scope.form.applicationForm
Expand Down Expand Up @@ -211,6 +221,7 @@ ShortFormApplicationController = (
$scope.applicant[fieldToDisable] = false

$scope.beginApplication = (lang = 'en') ->
AnalyticsService.trackApplicationStart($scope.listing.Id, AccountService.loggedInUser?.id || null, 'Begin Application')
if $scope.isCustomEducatorListing()
ShortFormNavigationService.goToApplicationPage('dahlia.short-form-welcome.custom-educator-screening', {lang: lang})
else if $scope.listing.Reserved_community_type
Expand Down Expand Up @@ -889,6 +900,7 @@ ShortFormApplicationController = (
.then( ->
ShortFormNavigationService.isLoading(false)
ShortFormNavigationService.goToApplicationPage('dahlia.short-form-application.confirmation')
AnalyticsService.trackApplicationComplete($scope.listing.Id, AccountService.loggedInUser?.id || null)
).catch( ->
ShortFormNavigationService.isLoading(false)
)
Expand All @@ -903,7 +915,9 @@ ShortFormApplicationController = (
# if redirecting to the React my-applications page, disable the "Leave site?" popup
if $window.ACCOUNT_INFORMATION_PAGES_REACT is "true"
$window.removeEventListener('beforeunload', ShortFormApplicationService.onExit)
$window.removeEventListener('unload', $scope.onUnload)

AnalyticsService.trackApplicationAbandon($scope.listing.Id, AccountService.loggedInUser.id, 'Logged In Save and Finish Later')
# ShortFormNavigationService.isLoading(false) will happen after My Apps are loaded
# go to my applications without tracking Form Success
$scope.go('dahlia.my-applications', {skipConfirm: true})
Expand All @@ -912,6 +926,7 @@ ShortFormApplicationController = (
)
else
ShortFormNavigationService.isLoading(false)
AnalyticsService.trackApplicationAbandon($scope.listing.Id, null, 'Logged Out Save and Finish Later')
# go to Create Account without tracking Form Success
$scope.go('dahlia.short-form-application.create-account')

Expand Down Expand Up @@ -971,17 +986,22 @@ ShortFormApplicationController = (
if $scope.appIsSubmitted(previousApp)
# My Applications page is now in React, prevent the "Leave Site?" popup when redirecting
$window.removeEventListener('beforeunload', ShortFormApplicationService.onExit)
$window.removeEventListener('unload', $scope.onUnload)
doubleSubmit = !! $scope.appIsSubmitted($scope.application)
if $window.ACCOUNT_INFORMATION_PAGES_REACT is "true"
currentUrl = window.location.origin
newUrl = "#{currentUrl}/my-applications?"
if previousApp.id
# user id should always be present, but we are being cautious
AnalyticsService.trackApplicationAbandon($scope.listing.Id, AccountService.loggedInUser?.id, 'Already Submitted')
newUrl += "alreadySubmittedId=#{previousApp.id}"
if doubleSubmit
newUrl += "&"
if doubleSubmit
# As we rebuilt the My Applications page in React we were not able to figure out a way to trigger the Double Submit Modal.
# We are leaving the code here both to document past behavior and to protect the application in case somehow the modal is triggered
# user id should always be present, but we are being cautious
AnalyticsService.trackApplicationAbandon($scope.listing.Id, AccountService.loggedInUser?.id, 'Double Submitted')
newUrl += "doubleSubmit=true"

window.location.href = newUrl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,9 @@ ShortFormApplicationService = (

# this will setup Service.application with the loaded data
Service.resetApplicationData(formattedApp)
if !_.isEmpty(data.application) && !_.isEmpty(Service.application) && Service.application.status.match(/draft/i) && Service.application.id
AnalyticsService.trackApplicationStart(data?.application?.listingID, data?.application?.primaryApplicant?.webAppID, 'Continue with these details')

# one last step, reconcile any uploaded files with your saved member + preference data
if !_.isEmpty(Service.application) && Service.application.status.match(/draft/i)
Service.refreshPreferences('all')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ShortFormNavigationService = (
Service.goToApplicationPage = (path, params) ->
# Every time the user completes an application page,
# we track that in GTM/GA as a form success.
AnalyticsService.trackFormSuccess('Application')
AnalyticsService.trackFormSuccess('Application Page View')
if params
$state.go(path, params)
else
Expand Down

0 comments on commit ba927d9

Please sign in to comment.