Skip to content

Commit

Permalink
chore: add analytics tracking to capture PlanningConstraints & `Pro…
Browse files Browse the repository at this point in the history
…pertyInformation` "overrides" (#3510)
  • Loading branch information
jessicamcinchak authored Aug 14, 2024
1 parent 6cac0af commit 65c8f29
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 18 deletions.
1 change: 1 addition & 0 deletions api.planx.uk/modules/gis/service/classifiedRoads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const classifiedRoadsSearch = async (
text: `is on a Classified Road`,
data: features.map((feature: any) => ({

Check warning on line 129 in api.planx.uk/modules/gis/service/classifiedRoads.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
name: `${feature.properties["RoadName1"]} - ${feature.properties["RoadClassification"]}`,
entity: feature.properties["GmlID"], // match Planning Data "entity" identifier for convenience when reporting inaccurate constraints
properties: feature.properties,
})),
category: "General policy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ const ALLOW_LIST = [
"application.declaration.connection",
"drawBoundary.action",
"findProperty.action",
"_overrides",
"planningConstraints.action",
"property.constraints.planning",
"property.type",
"propertyInformation.action",
"proposal.projectType",
"usedFOIYNPP",
"user.role",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ function Component(props: Props) {
siteBoundary,
{ x, y, longitude, latitude, usrn },
hasPlanningData,
priorOverrides,
] = useStore((state) => [
state.currentCard?.id,
state.cachedBreadcrumbs,
state.teamSlug,
state.computePassport().data?.["property.boundary.site"],
(state.computePassport().data?.["_address"] as SiteAddress) || {},
state.teamIntegrations?.hasPlanningData,
state.computePassport().data?.["_overrides"],
]);

// PlanningConstraints must come after at least a FindProperty in the graph
Expand All @@ -66,7 +68,7 @@ function Component(props: Props) {
// still prepopulate any previously marked inaccurateConstraints
const initialInaccurateConstraints =
currentCardId &&
cachedBreadcrumbs?.[currentCardId]?.["data"]?.["_overrides"];
cachedBreadcrumbs?.[currentCardId]?.["data"]?.["_overrides"]?.[props.fn];
const [inaccurateConstraints, setInaccurateConstraints] =
useState<InaccurateConstraints>(initialInaccurateConstraints);

Expand Down Expand Up @@ -176,7 +178,13 @@ function Component(props: Props) {
if (data) _constraints.push(data as GISResponse["constraints"]);
}

const _overrides = inaccurateConstraints;
const hasInaccurateConstraints = inaccurateConstraints && Object.keys(inaccurateConstraints).length > 0;
const _overrides = hasInaccurateConstraints ? { ...priorOverrides, [props.fn]: inaccurateConstraints } : undefined;

// `planningConstraints.action` is for analytics
const userAction = hasInaccurateConstraints
? "Reported at least one inaccurate planning constraint"
: "Accepted all planning constraints";

// `[props.fn]` & `_nots[props.fn]` are responsible for future service automations
const _nots: IntersectingConstraints = {};
Expand Down Expand Up @@ -206,8 +214,9 @@ function Component(props: Props) {
const passportData = {
_constraints,
_overrides,
"planningConstraints.action": userAction,
_nots: notsAfterOverrides,
...intersectingConstraintsAfterOverrides,
...(intersectingConstraintsAfterOverrides[props.fn]?.length === 0 ? undefined : intersectingConstraintsAfterOverrides),
};

props.handleSubmit?.({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export const mockConstraints: GISResponse["constraints"] = {
"data": [
{
"name": "Black Prince Road - Classified Unnumbered",
"entity": "Highways_RoadLink.83155",
"properties": {
"GmlID": "Highways_RoadLink.83155",
"OBJECTID": 83155,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("handleOverrides with non-granular constraint categories", () => {
test("when all entities in a constraint category are inaccurate, the whole category no longer applies", () => {
const overrides = {
"road.classified": {
"entities": ['undefined'], // @todo roads don't have entity ids, record the property.OBJECTID via OS?
"entities": ["Highways_RoadLink.83155"],
"reason": "My house is on a corner, and this is not the road that my entry faces"
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ function Component(props: PublicProps<PropertyInformation>) {
titleBoundary={passport.data?.["property.boundary.title"]}
blpuCodes={blpuCodes}
overrideAnswer={overrideAnswer}
handleSubmit={props.handleSubmit}
handleSubmit={() => {
// If the user changed their property type, they'll already have a previous PropertyInformation breadcrumb that set `_overrides`
const hasOverrodeAnswer = passport.data?.["_overrides"]?.["property.type"];
const passportData = {
"propertyInformation.action": hasOverrodeAnswer ? "Changed the property type" : "Accepted the property type",
};

props.handleSubmit?.({
data: passportData,
});
}}
/>
) : (
<Card>
Expand Down
1 change: 0 additions & 1 deletion editor.planx.uk/src/lib/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// add/edit/remove feature flags in array below
const AVAILABLE_FEATURE_FLAGS = [
"SEARCH",
"OVERRIDE_CONSTRAINTS",
"TREES",
"ADD_NEW_EDITOR",
] as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,22 @@ test("it clears the correct breadcrumb and navigates back to the right node", as
["residential.dwelling.house.semiDetached"],
);

// override answer
// override answer ("I'm in a flat not a house")
overrideAnswer("property.type");

// confirm we've cleared the provided passport variable from the first node that set it
// confirm we've cleared the provided passport variable from the first node that set it but added an audit variable `_overrides`
const afterOverrideBreadcrumb = getState().breadcrumbs;
const addressBreadcrumb: any =
afterOverrideBreadcrumb?.["FindPropertyNodeId"]?.data;
expect(Object.keys(addressBreadcrumb)).toHaveLength(3);
expect(Object.keys(addressBreadcrumb)).toHaveLength(4);
expect(addressBreadcrumb).not.toHaveProperty(["property.type"]);
expect(addressBreadcrumb).toHaveProperty(["_overrides"]);

const afterOverridePassport = getState().computePassport();
expect(afterOverridePassport.data).toBeDefined();
expect(afterOverridePassport.data).not.toHaveProperty(["property.type"]);

// confirm we've stored a copy of the original value in the first node that set it
const overrideData: any =
afterOverrideBreadcrumb?.["FindPropertyNodeId"]?.override;
expect(overrideData).toEqual({
expect(afterOverridePassport.data).toHaveProperty(["_overrides"])
expect(afterOverridePassport.data?.["_overrides"]).toEqual({
"property.type": ["residential.dwelling.house.semiDetached"],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ export const ALLOW_LIST = [
"application.declaration.connection",
"drawBoundary.action",
"findProperty.action",
"_overrides",
"planningConstraints.action",
"property.constraints.planning",
"property.type",
"propertyInformation.action",
"proposal.projectType",
"usedFOIYNPP",
"user.role",
Expand Down
8 changes: 4 additions & 4 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,11 +617,11 @@ export const previewStore: StateCreator<

if (originalNodeId) {
// Omit existing passport value from breadcrumbs.data in whichever node originally set it, so it won't be auto-answered in future
// and keep a receipt of the original value in breadcrumbs.override
// and keep a receipt of the original value in breadcrumbs.data._overrides
record(originalNodeId, {
data: omit(breadcrumbs?.[originalNodeId]?.data, fn),
override: {
[fn]: breadcrumbs?.[originalNodeId]?.data?.[fn],
data: {
...omit(breadcrumbs?.[originalNodeId]?.data, fn),
_overrides: { [fn]: breadcrumbs?.[originalNodeId]?.data?.[fn] },
},
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
CREATE OR REPLACE VIEW "public"."analytics_summary" AS
SELECT a.id AS analytics_id,
al.id AS analytics_log_id,
f.slug AS service_slug,
t.slug AS team_slug,
a.type AS analytics_type,
al.created_at AS analytics_log_created_at,
a.created_at AS analytics_created_at,
((a.user_agent -> 'os'::text) ->> 'name'::text) AS operating_system,
((a.user_agent -> 'browser'::text) ->> 'name'::text) AS browser,
((a.user_agent -> 'platform'::text) ->> 'type'::text) AS platform,
a.referrer,
al.flow_direction,
(al.metadata ->> 'change'::text) AS change_metadata,
(al.metadata ->> 'back'::text) AS back_metadata,
(al.metadata ->> 'selectedUrls'::text) AS selected_urls,
(al.metadata ->> 'flag'::text) AS result_flag,
(al.metadata -> 'flagSet'::text) AS result_flagset,
((al.metadata -> 'displayText'::text) ->> 'heading'::text) AS result_heading,
((al.metadata -> 'displayText'::text) ->> 'description'::text) AS result_description,
(al.metadata -> 'helpTextUseful'::text) AS help_text_useful,
CASE
WHEN al.has_clicked_help THEN al.metadata
ELSE NULL::jsonb
END AS help_metadata,
al.user_exit AS is_user_exit,
al.node_type,
al.node_title,
al.has_clicked_help,
al.input_errors,
(date_part('epoch'::text, (al.next_log_created_at - al.created_at)))::numeric(10,1) AS time_spent_on_node_seconds,
a.ended_at AS analytics_ended_at,
((date_part('epoch'::text, (a.ended_at - a.created_at)) / (60)::double precision))::numeric(10,1) AS time_spent_on_analytics_session_minutes,
al.node_id,
al.allow_list_answers,
(al.allow_list_answers -> 'proposal.projectType'::text) AS proposal_project_type,
(al.allow_list_answers -> 'application.declaration.connection'::text) AS application_declaration_connection,
(al.allow_list_answers -> 'property.type'::text) AS property_type,
(al.allow_list_answers -> 'drawBoundary.action'::text) AS draw_boundary_action,
(al.allow_list_answers -> 'user.role'::text) AS user_role,
(al.allow_list_answers -> 'property.constraints.planning'::text) AS property_constraints_planning,
(al.allow_list_answers -> 'findProperty.action'::text) AS find_property_action,
(al.allow_list_answers -> 'usedFOIYNPP'::text) AS used_foiynpp
FROM (((analytics a
LEFT JOIN analytics_logs al ON ((a.id = al.analytics_id)))
LEFT JOIN flows f ON ((a.flow_id = f.id)))
LEFT JOIN teams t ON ((t.id = f.team_id)));

CREATE OR REPLACE VIEW "public"."submission_services_summary" AS
WITH resumes_per_session AS (
SELECT reconciliation_requests.session_id,
count(reconciliation_requests.id) AS number_times_resumed
FROM reconciliation_requests
GROUP BY reconciliation_requests.session_id
), bops_agg AS (
SELECT bops_applications.session_id,
json_agg(json_build_object('id', bops_applications.bops_id, 'submittedAt', bops_applications.created_at, 'destinationUrl', bops_applications.destination_url) ORDER BY bops_applications.created_at DESC) AS bops_applications
FROM bops_applications
GROUP BY bops_applications.session_id
), email_agg AS (
SELECT email_applications.session_id,
json_agg(json_build_object('id', email_applications.id, 'recipient', email_applications.recipient, 'submittedAt', email_applications.created_at) ORDER BY email_applications.created_at DESC) AS email_applications
FROM email_applications
GROUP BY email_applications.session_id
), uniform_agg AS (
SELECT uniform_applications.submission_reference,
json_agg(json_build_object('id', uniform_applications.idox_submission_id, 'submittedAt', uniform_applications.created_at) ORDER BY uniform_applications.created_at DESC) AS uniform_applications
FROM uniform_applications
GROUP BY uniform_applications.submission_reference
), payment_requests_agg AS (
SELECT payment_requests.session_id,
json_agg(json_build_object('id', payment_requests.id, 'createdAt', payment_requests.created_at, 'paidAt', payment_requests.paid_at, 'govpayPaymentId', payment_requests.govpay_payment_id) ORDER BY payment_requests.created_at DESC) AS payment_requests
FROM payment_requests
GROUP BY payment_requests.session_id
), payment_status_agg AS (
SELECT payment_status.session_id,
json_agg(json_build_object('govpayPaymentId', payment_status.payment_id, 'createdAt', payment_status.created_at, 'status', payment_status.status) ORDER BY payment_status.created_at DESC) AS payment_status
FROM payment_status
GROUP BY payment_status.session_id
), s3_agg AS (
SELECT s3_applications.session_id,
json_agg(json_build_object('id', s3_applications.id, 'submittedAt', s3_applications.created_at) ORDER BY s3_applications.created_at DESC) AS s3_applications
FROM s3_applications
GROUP BY s3_applications.session_id
)
SELECT (ls.id)::text AS session_id,
t.slug AS team_slug,
f.slug AS service_slug,
ls.created_at,
ls.submitted_at,
((ls.submitted_at)::date - (ls.created_at)::date) AS session_length_days,
ls.has_user_saved AS user_clicked_save,
rps.number_times_resumed,
ls.allow_list_answers,
(ls.allow_list_answers -> 'proposal.projectType'::text) AS proposal_project_type,
(ls.allow_list_answers -> 'application.declaration.connection'::text) AS application_declaration_connection,
(ls.allow_list_answers -> 'property.type'::text) AS property_type,
(ls.allow_list_answers -> 'drawBoundary.action'::text) AS draw_boundary_action,
(ls.allow_list_answers -> 'user.role'::text) AS user_role,
(ls.allow_list_answers -> 'property.constraints.planning'::text) AS property_constraints_planning,
CASE
WHEN (((pr.payment_requests)::jsonb IS NOT NULL) AND (jsonb_array_length((pr.payment_requests)::jsonb) > 0)) THEN true
ELSE false
END AS user_invited_to_pay,
pr.payment_requests,
ps.payment_status,
CASE
WHEN (((ba.bops_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ba.bops_applications)::jsonb) > 0)) THEN true
ELSE false
END AS sent_to_bops,
ba.bops_applications,
CASE
WHEN (((ua.uniform_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ua.uniform_applications)::jsonb) > 0)) THEN true
ELSE false
END AS sent_to_uniform,
ua.uniform_applications,
CASE
WHEN (((ea.email_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ea.email_applications)::jsonb) > 0)) THEN true
ELSE false
END AS sent_to_email,
ea.email_applications,
(ls.allow_list_answers -> 'findProperty.action'::text) AS find_property_action,
CASE
WHEN (((sa.s3_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((sa.s3_applications)::jsonb) > 0)) THEN true
ELSE false
END AS sent_to_s3_power_automate,
sa.s3_applications
FROM (((((((((lowcal_sessions ls
LEFT JOIN flows f ON ((f.id = ls.flow_id)))
LEFT JOIN teams t ON ((t.id = f.team_id)))
LEFT JOIN resumes_per_session rps ON ((rps.session_id = (ls.id)::text)))
LEFT JOIN payment_requests_agg pr ON ((pr.session_id = ls.id)))
LEFT JOIN payment_status_agg ps ON ((ps.session_id = ls.id)))
LEFT JOIN bops_agg ba ON ((ba.session_id = (ls.id)::text)))
LEFT JOIN uniform_agg ua ON ((ua.submission_reference = (ls.id)::text)))
LEFT JOIN email_agg ea ON ((ea.session_id = ls.id)))
LEFT JOIN s3_agg sa ON ((sa.session_id = (ls.id)::text)))
WHERE ((f.slug IS NOT NULL) AND (t.slug IS NOT NULL));
Loading

0 comments on commit 65c8f29

Please sign in to comment.