From d335f9c7dc9d0926fd87d545ead698c369bd508c Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Thu, 21 Dec 2023 14:52:05 +0000 Subject: [PATCH 1/5] feat: track node_fn and allow_list_answers in analytics logs - The passport variable a node can relate to is stored either on the `fn` or the `val` of the node. - Begin tracking this to support allow list work - These allowlist answers will always be an empty array when the log is created - Added update permission as on breadcrumb changes the record will be updated if the logs node_fn is in the allow_list --- .../src/pages/FlowEditor/lib/analyticsProvider.tsx | 6 ++++++ hasura.planx.uk/metadata/tables.yaml | 2 ++ .../migrations/1704731606083_squashed/down.sql | 8 ++++++++ .../migrations/1704731606083_squashed/up.sql | 10 ++++++++++ 4 files changed, 26 insertions(+) create mode 100644 hasura.planx.uk/migrations/1704731606083_squashed/down.sql create mode 100644 hasura.planx.uk/migrations/1704731606083_squashed/up.sql diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index 5595fcad26..cf663c1869 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -168,6 +168,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ const metadata: NodeMetadata = getNodeMetadata(nodeToTrack, nodeId); const nodeType = nodeToTrack?.type ? TYPES[nodeToTrack.type] : null; const nodeTitle = extractNodeTitle(nodeToTrack); + const nodeFn = nodeToTrack?.data?.fn || nodeToTrack?.data?.val; const result = await insertNewAnalyticsLog( logDirection, @@ -176,6 +177,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ nodeType, nodeTitle, nodeId, + nodeFn, ); const { id, created_at: newLogCreatedAt } = @@ -208,6 +210,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ nodeType: string | null, nodeTitle: string, nodeId: string | null, + nodeFn: string | null, ) { const result = await publicClient.mutate({ mutation: gql` @@ -218,6 +221,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ $node_type: String $node_title: String $node_id: String + $node_fn: String ) { insert_analytics_logs_one( object: { @@ -228,6 +232,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ node_type: $node_type node_title: $node_title node_id: $node_id + node_fn: $node_fn } ) { id @@ -242,6 +247,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ node_type: nodeType, node_title: nodeTitle, node_id: nodeId, + node_fn: nodeFn, }, }); return result; diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index 24e17de3b8..af9b3e2b4c 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -40,6 +40,7 @@ - id - input_errors - metadata + - node_fn - node_id - node_title - node_type @@ -57,6 +58,7 @@ - role: public permission: columns: + - allow_list_answers - flow_direction - has_clicked_help - input_errors diff --git a/hasura.planx.uk/migrations/1704731606083_squashed/down.sql b/hasura.planx.uk/migrations/1704731606083_squashed/down.sql new file mode 100644 index 0000000000..7f022dc747 --- /dev/null +++ b/hasura.planx.uk/migrations/1704731606083_squashed/down.sql @@ -0,0 +1,8 @@ + +comment on column "public"."analytics_logs"."allow_list_answers" is NULL; + +comment on column "public"."analytics_logs"."node_fn" is NULL; + +alter table "public"."analytics_logs" drop column "node_fn"; + +alter table "public"."analytics_logs" drop column "allow_list_answers"; \ No newline at end of file diff --git a/hasura.planx.uk/migrations/1704731606083_squashed/up.sql b/hasura.planx.uk/migrations/1704731606083_squashed/up.sql new file mode 100644 index 0000000000..cef41e62f7 --- /dev/null +++ b/hasura.planx.uk/migrations/1704731606083_squashed/up.sql @@ -0,0 +1,10 @@ + +alter table "public"."analytics_logs" add column "node_fn" text + null; + +alter table "public"."analytics_logs" add column "allow_list_answers" JSONB + null default '[]'::jsonb; + +comment on column "public"."analytics_logs"."node_fn" is E'The passport variable a node can relate to as stored on the `fn` or `val` of the node'; + +comment on column "public"."analytics_logs"."allow_list_answers" is E'If the node sets a passport variable deemed as safe to track then any answers are stored in this field'; From 152ca8f65d67fef12760993155c10c8a30606d12 Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Thu, 21 Dec 2023 15:49:00 +0000 Subject: [PATCH 2/5] feat: track allow list answers for non auto answer questions - When breadcrumbs change and the user has answered a question check if it's in the allow list and if so store - Update permission to allow us to select the node_id - This allows us to ensure that we'll only track allow list answers against the correct nodes --- .../FlowEditor/lib/analyticsProvider.tsx | 58 ++++++++++++++++++- hasura.planx.uk/metadata/tables.yaml | 1 + 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index cf663c1869..3801461353 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -20,6 +20,11 @@ type AnalyticsLogDirection = | "reset" | "save"; +const allowList = [ + "proposal.projectType", + "application.declaration.connection", +]; + export type HelpClickMetadata = Record; export type SelectedUrlsMetadata = Record<"selectedUrls", string[]>; export type BackwardsNavigationInitiatorType = "change" | "back"; @@ -132,7 +137,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(onVisibilityChange, []); useEffect(() => { - if (shouldTrackAnalytics && analyticsId) trackAutoTrueNodes(); + if (shouldTrackAnalytics && analyticsId) trackBreadcrumbChanges(); }, [breadcrumbs]); return ( @@ -417,6 +422,53 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ } } + async function updateLastVisibleNodeLogWithAllowListAnswers(nodeId: string) { + if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { + const allowListAnswers = getAllowListAnswers(nodeId); + if (allowListAnswers) { + await publicClient.mutate({ + mutation: gql` + mutation UpdateAllowListAnswers( + $id: bigint! + $allow_list_answers: jsonb + $node_id: String! + ) { + update_analytics_logs( + // Update the analytics log for the last visible node but + // additionally check that the node_ids match to avoid + // incorrectly capturing incorrect data + where: { id: { _eq: $id }, node_id: { _eq: $node_id } } + _set: { allow_list_answers: $allow_list_answers } + ) { + returning { + id + } + } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + allow_list_answers: allowListAnswers, + node_id: nodeId, + }, + }); + } + } + } + + function getAllowListAnswers(nodeId: string) { + const { data } = flow[nodeId]; + const nodeFn = data?.fn || data?.val; + if (nodeFn && allowList.includes(nodeFn)) { + const answerIds = breadcrumbs[nodeId]?.answers; + const answerValues = answerIds?.map((answerId) => { + return flow[answerId]?.data?.val; + }); + const filteredAnswers = answerValues?.filter(Boolean); + return filteredAnswers; + } + } + function getNodeMetadata(node: Store.node, nodeId: string) { const isAutoAnswered = breadcrumbs[nodeId]?.auto || false; switch (node?.type) { @@ -502,13 +554,15 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ } } - function trackAutoTrueNodes() { + function trackBreadcrumbChanges() { const updatedBreadcrumbKeys = findUpdatedBreadcrumbKeys(); if (updatedBreadcrumbKeys) { updatedBreadcrumbKeys.forEach((breadcrumbKey) => { const breadcrumb = breadcrumbs[breadcrumbKey]; if (breadcrumb.auto) { track(breadcrumbKey); + } else { + updateLastVisibleNodeLogWithAllowListAnswers(breadcrumbKey); } }); } diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index af9b3e2b4c..c61c2f5e2a 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -52,6 +52,7 @@ - analytics_id - created_at - id + - node_id - user_exit filter: {} update_permissions: From cdb0883584173896b86e6b5a2bfb5b6886e58aaf Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Thu, 21 Dec 2023 16:30:45 +0000 Subject: [PATCH 3/5] fix: broken mutation due to incorrect comment syntax --- editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index 3801461353..50110ac66f 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -434,9 +434,6 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ $node_id: String! ) { update_analytics_logs( - // Update the analytics log for the last visible node but - // additionally check that the node_ids match to avoid - // incorrectly capturing incorrect data where: { id: { _eq: $id }, node_id: { _eq: $node_id } } _set: { allow_list_answers: $allow_list_answers } ) { From b0eda998870faac0133d8b02eb0c915772f69684 Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Mon, 8 Jan 2024 14:28:34 +0000 Subject: [PATCH 4/5] refactor: update allow list constant - As per: https://github.com/theopensystemslab/planx-new/pull/2597#discussion_r1441600442 - Update definition to use uppercase variable name and declare as const for consistency and more thorough type defintion --- .../src/pages/FlowEditor/lib/analyticsProvider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index 50110ac66f..5ae3ac1b89 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -20,10 +20,10 @@ type AnalyticsLogDirection = | "reset" | "save"; -const allowList = [ +const ALLOW_LIST = [ "proposal.projectType", "application.declaration.connection", -]; +] as const; export type HelpClickMetadata = Record; export type SelectedUrlsMetadata = Record<"selectedUrls", string[]>; @@ -456,7 +456,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ function getAllowListAnswers(nodeId: string) { const { data } = flow[nodeId]; const nodeFn = data?.fn || data?.val; - if (nodeFn && allowList.includes(nodeFn)) { + if (nodeFn && ALLOW_LIST.includes(nodeFn)) { const answerIds = breadcrumbs[nodeId]?.answers; const answerValues = answerIds?.map((answerId) => { return flow[answerId]?.data?.val; From 612fa877f30818b4d22c98df3e3b44158bdfa095 Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Mon, 8 Jan 2024 16:10:57 +0000 Subject: [PATCH 5/5] refactor: try to reduce nesting with guard clauses - As per: https://github.com/theopensystemslab/planx-new/pull/2597#discussion_r1441607544 - Reduce nesting by refactoring to use guard clauses - Abstract commonly used guard clause into it's own function. --- .../FlowEditor/lib/analyticsProvider.tsx | 365 +++++++++--------- 1 file changed, 183 insertions(+), 182 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index 5ae3ac1b89..6ba180f40b 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -107,21 +107,22 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ const previousBreadcrumbs = usePrevious(breadcrumbs); const trackVisibilityChange = () => { - if (lastVisibleNodeAnalyticsLogId && shouldTrackAnalytics) { - if (document.visibilityState === "hidden") { + skipUpdateIfNotTracking(); + switch (document.visibilityState) { + case "hidden": send( `${ process.env.REACT_APP_API_URL - }/analytics/log-user-exit?analyticsLogId=${lastVisibleNodeAnalyticsLogId.toString()}`, + }/analytics/log-user-exit?analyticsLogId=${lastVisibleNodeAnalyticsLogId?.toString()}`, ); - } - if (document.visibilityState === "visible") { + break; + case "visible": send( `${ process.env.REACT_APP_API_URL }/analytics/log-user-resume?analyticsLogId=${lastVisibleNodeAnalyticsLogId?.toString()}`, ); - } + break; } }; @@ -137,7 +138,8 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(onVisibilityChange, []); useEffect(() => { - if (shouldTrackAnalytics && analyticsId) trackBreadcrumbChanges(); + if (!shouldTrackAnalytics || !analyticsId) return; + trackBreadcrumbChanges(); }, [breadcrumbs]); return ( @@ -283,174 +285,175 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ }); } + function skipUpdateIfNotTracking() { + if (!shouldTrackAnalytics || !lastVisibleNodeAnalyticsLogId) { + return; + } + } + async function trackHelpClick(metadata?: HelpClickMetadata) { - if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { - await publicClient.mutate({ - mutation: gql` - mutation UpdateHasClickedHelp($id: bigint!, $metadata: jsonb = {}) { - update_analytics_logs_by_pk( - pk_columns: { id: $id } - _set: { has_clicked_help: true } - _append: { metadata: $metadata } - ) { - id - } + skipUpdateIfNotTracking(); + await publicClient.mutate({ + mutation: gql` + mutation UpdateHasClickedHelp($id: bigint!, $metadata: jsonb = {}) { + update_analytics_logs_by_pk( + pk_columns: { id: $id } + _set: { has_clicked_help: true } + _append: { metadata: $metadata } + ) { + id } - `, - variables: { - id: lastVisibleNodeAnalyticsLogId, - metadata, - }, - }); - } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + metadata, + }, + }); } async function trackNextStepsLinkClick(metadata?: SelectedUrlsMetadata) { - if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { - await publicClient.mutate({ - mutation: gql` - mutation UpdateHasClickNextStepsLink( - $id: bigint! - $metadata: jsonb = {} + skipUpdateIfNotTracking(); + await publicClient.mutate({ + mutation: gql` + mutation UpdateHasClickNextStepsLink( + $id: bigint! + $metadata: jsonb = {} + ) { + update_analytics_logs_by_pk( + pk_columns: { id: $id } + _append: { metadata: $metadata } ) { - update_analytics_logs_by_pk( - pk_columns: { id: $id } - _append: { metadata: $metadata } - ) { - id - } + id } - `, - variables: { - id: lastVisibleNodeAnalyticsLogId, - metadata, - }, - }); - } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + metadata, + }, + }); } async function trackFlowDirectionChange( flowDirection: AnalyticsLogDirection, ) { - if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { - await publicClient.mutate({ - mutation: gql` - mutation UpdateFlowDirection($id: bigint!, $flow_direction: String) { - update_analytics_logs_by_pk( - pk_columns: { id: $id } - _set: { flow_direction: $flow_direction } - ) { - id - } + skipUpdateIfNotTracking(); + await publicClient.mutate({ + mutation: gql` + mutation UpdateFlowDirection($id: bigint!, $flow_direction: String) { + update_analytics_logs_by_pk( + pk_columns: { id: $id } + _set: { flow_direction: $flow_direction } + ) { + id } - `, - variables: { - id: lastVisibleNodeAnalyticsLogId, - flow_direction: flowDirection, - }, - }); - } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + flow_direction: flowDirection, + }, + }); } async function trackBackwardsNavigation( initiator: BackwardsNavigationInitiatorType, nodeId?: string, ) { + skipUpdateIfNotTracking(); const targetNodeMetadata = nodeId ? getTargetNodeDataFromFlow(nodeId) : {}; const metadata: Record = {}; metadata[`${initiator}`] = targetNodeMetadata; - if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { - await publicClient.mutate({ - mutation: gql` - mutation UpdateHaInitiatedBackwardsNavigation( - $id: bigint! - $metadata: jsonb = {} + await publicClient.mutate({ + mutation: gql` + mutation UpdateHaInitiatedBackwardsNavigation( + $id: bigint! + $metadata: jsonb = {} + ) { + update_analytics_logs_by_pk( + pk_columns: { id: $id } + _append: { metadata: $metadata } ) { - update_analytics_logs_by_pk( - pk_columns: { id: $id } - _append: { metadata: $metadata } - ) { - id - } + id } - `, - variables: { - id: lastVisibleNodeAnalyticsLogId, - metadata, - }, - }); - } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + metadata, + }, + }); } async function createAnalytics(type: AnalyticsType) { - if (shouldTrackAnalytics) { - const userAgent = Bowser.parse(window.navigator.userAgent); - const referrer = document.referrer || null; - - const response = await publicClient.mutate({ - mutation: gql` - mutation InsertNewAnalytics( - $type: String - $flow_id: uuid - $user_agent: jsonb - $referrer: String - ) { - insert_analytics_one( - object: { - type: $type - flow_id: $flow_id - user_agent: $user_agent - referrer: $referrer - } - ) { - id + if (!shouldTrackAnalytics) return; + const userAgent = Bowser.parse(window.navigator.userAgent); + const referrer = document.referrer || null; + + const response = await publicClient.mutate({ + mutation: gql` + mutation InsertNewAnalytics( + $type: String + $flow_id: uuid + $user_agent: jsonb + $referrer: String + ) { + insert_analytics_one( + object: { + type: $type + flow_id: $flow_id + user_agent: $user_agent + referrer: $referrer } + ) { + id } - `, - variables: { - type, - flow_id: flowId, - user_agent: userAgent, - referrer, - }, - }); - const id = response.data.insert_analytics_one.id; - setAnalyticsId(id); - const currentNodeId = currentCard()?.id; - if (currentNodeId) track(currentNodeId, type, id); - } + } + `, + variables: { + type, + flow_id: flowId, + user_agent: userAgent, + referrer, + }, + }); + const id = response.data.insert_analytics_one.id; + setAnalyticsId(id); + const currentNodeId = currentCard()?.id; + if (currentNodeId) track(currentNodeId, type, id); } async function updateLastVisibleNodeLogWithAllowListAnswers(nodeId: string) { - if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { - const allowListAnswers = getAllowListAnswers(nodeId); - if (allowListAnswers) { - await publicClient.mutate({ - mutation: gql` - mutation UpdateAllowListAnswers( - $id: bigint! - $allow_list_answers: jsonb - $node_id: String! - ) { - update_analytics_logs( - where: { id: { _eq: $id }, node_id: { _eq: $node_id } } - _set: { allow_list_answers: $allow_list_answers } - ) { - returning { - id - } - } + skipUpdateIfNotTracking(); + + const allowListAnswers = getAllowListAnswers(nodeId); + if (!allowListAnswers) return; + + await publicClient.mutate({ + mutation: gql` + mutation UpdateAllowListAnswers( + $id: bigint! + $allow_list_answers: jsonb + $node_id: String! + ) { + update_analytics_logs( + where: { id: { _eq: $id }, node_id: { _eq: $node_id } } + _set: { allow_list_answers: $allow_list_answers } + ) { + returning { + id } - `, - variables: { - id: lastVisibleNodeAnalyticsLogId, - allow_list_answers: allowListAnswers, - node_id: nodeId, - }, - }); - } - } + } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + allow_list_answers: allowListAnswers, + node_id: nodeId, + }, + }); } function getAllowListAnswers(nodeId: string) { @@ -502,24 +505,23 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ * Capture user input errors caught by ErrorWrapper component */ async function trackInputErrors(error: string) { - if (shouldTrackAnalytics && lastVisibleNodeAnalyticsLogId) { - await publicClient.mutate({ - mutation: gql` - mutation TrackInputErrors($id: bigint!, $error: jsonb) { - update_analytics_logs_by_pk( - pk_columns: { id: $id } - _append: { input_errors: $error } - ) { - id - } + skipUpdateIfNotTracking(); + await publicClient.mutate({ + mutation: gql` + mutation TrackInputErrors($id: bigint!, $error: jsonb) { + update_analytics_logs_by_pk( + pk_columns: { id: $id } + _append: { input_errors: $error } + ) { + id } - `, - variables: { - id: lastVisibleNodeAnalyticsLogId, - error, - }, - }); - } + } + `, + variables: { + id: lastVisibleNodeAnalyticsLogId, + error, + }, + }); } function extractNodeTitle(node: Store.node) { @@ -531,38 +533,37 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ } function determineLogDirection() { - if (previousBreadcrumbs) { - const curLength = Object.keys(breadcrumbs).length; - const prevLength = Object.keys(previousBreadcrumbs).length; - if (curLength > prevLength) return "forwards"; - if (curLength < prevLength) return "backwards"; - } + if (!previousBreadcrumbs) return; + + const curLength = Object.keys(breadcrumbs).length; + const prevLength = Object.keys(previousBreadcrumbs).length; + if (curLength > prevLength) return "forwards"; + if (curLength < prevLength) return "backwards"; } function findUpdatedBreadcrumbKeys(): string[] | undefined { - if (previousBreadcrumbs) { - const currentKeys = Object.keys(breadcrumbs); - const previousKeys = Object.keys(previousBreadcrumbs); + if (!previousBreadcrumbs) return; - const updatedBreadcrumbKeys = currentKeys.filter( - (breadcrumb) => !previousKeys.includes(breadcrumb), - ); - return updatedBreadcrumbKeys; - } + const currentKeys = Object.keys(breadcrumbs); + const previousKeys = Object.keys(previousBreadcrumbs); + const updatedBreadcrumbKeys = currentKeys.filter( + (breadcrumb) => !previousKeys.includes(breadcrumb), + ); + return updatedBreadcrumbKeys; } function trackBreadcrumbChanges() { const updatedBreadcrumbKeys = findUpdatedBreadcrumbKeys(); - if (updatedBreadcrumbKeys) { - updatedBreadcrumbKeys.forEach((breadcrumbKey) => { - const breadcrumb = breadcrumbs[breadcrumbKey]; - if (breadcrumb.auto) { - track(breadcrumbKey); - } else { - updateLastVisibleNodeLogWithAllowListAnswers(breadcrumbKey); - } - }); - } + if (!updatedBreadcrumbKeys) return; + + updatedBreadcrumbKeys.forEach((breadcrumbKey) => { + const breadcrumb = breadcrumbs[breadcrumbKey]; + if (breadcrumb.auto) { + track(breadcrumbKey); + } else { + updateLastVisibleNodeLogWithAllowListAnswers(breadcrumbKey); + } + }); } };