Skip to content

Commit

Permalink
Merge pull request #1197 from cityofaustin/jc-last-seen
Browse files Browse the repository at this point in the history
Adds basic user analytics tracking
  • Loading branch information
johnclary authored Nov 14, 2023
2 parents 35aa18a + fd525f6 commit d790dee
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 12 deletions.
37 changes: 35 additions & 2 deletions moped-database/metadata/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4548,6 +4548,37 @@
- type_order
- date_added
filter: {}
- table:
name: moped_user_events
schema: public
insert_permissions:
- role: moped-admin
permission:
check: {}
set:
user_id: x-hasura-user-db-id
columns:
- event_name
- user_id
comment: ""
- role: moped-editor
permission:
check: {}
set:
user_id: x-hasura-user-db-id
columns:
- event_name
- user_id
comment: ""
- role: moped-viewer
permission:
check: {}
set:
user_id: x-hasura-user-db-id
columns:
- event_name
- user_id
comment: ""
- table:
name: moped_user_followed_projects
schema: public
Expand Down Expand Up @@ -4663,11 +4694,11 @@
- is_deleted
- is_user_group_member
- last_name
- last_seen_date
- note
- picture
- roles
- title
- user_id
- workgroup_id
select_permissions:
- role: moped-admin
Expand All @@ -4681,6 +4712,7 @@
- is_deleted
- is_user_group_member
- last_name
- last_seen_date
- note
- picture
- roles
Expand All @@ -4700,6 +4732,7 @@
- is_deleted
- is_user_group_member
- last_name
- last_seen_date
- note
- picture
- roles
Expand All @@ -4719,6 +4752,7 @@
- is_deleted
- is_user_group_member
- last_name
- last_seen_date
- note
- picture
- roles
Expand All @@ -4743,7 +4777,6 @@
- picture
- roles
- title
- user_id
- workgroup_id
filter: {}
check: {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DROP TRIGGER set_user_last_seen_date_trigger;

DROP FUNCTION set_user_last_seen_date;

DROP TABLE "public"."moped_user_events";

ALTER TABLE "public"."moped_users" DROP COLUMN "last_seen_date";
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
ALTER TABLE "public"."moped_users"
ADD COLUMN "last_seen_date" timestamptz NULL;

COMMENT ON COLUMN moped_users.last_seen_date IS 'Tracks the last time a user loaded the Moped app in their browser. This value is set by the set_last_seen_date function. This value is not 100% reliable because it is updated by an API call that can be blocked by the client.';


CREATE TABLE "public"."moped_user_events" (
"id" serial NOT NULL,
"user_id" integer NOT NULL,
"event_name" text NOT NULL CHECK (event_name in('app_load')),
"created_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("id"),
FOREIGN KEY ("user_id") REFERENCES "public"."moped_users" ("user_id") ON UPDATE CASCADE ON DELETE CASCADE
);

COMMENT ON TABLE "public"."moped_user_events" IS E'Tracks user activities for analytics purposes';

CREATE FUNCTION public.set_user_last_seen_date ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE
moped_users
SET
last_seen_date = NEW.created_at
WHERE
user_id = NEW.user_id;
RETURN NEW;
END;
$$;

CREATE TRIGGER set_user_last_seen_date_trigger
AFTER INSERT ON public.moped_user_events
FOR EACH ROW
EXECUTE FUNCTION public.set_user_last_seen_date ();

COMMENT ON FUNCTION set_user_last_seen_date IS 'Updates the moped_users.last_seen_date based on inserts into the moped_user_events table';
15 changes: 9 additions & 6 deletions moped-editor/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { setContext } from "@apollo/client/link/context";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import ProjectListViewQueryContext from "./components/QueryContextProvider";
import ActivityMetrics from "./components/ActivityMetrics";

// Apollo GraphQL Client
import {
Expand Down Expand Up @@ -53,7 +54,7 @@ const useClient = (user) =>
project_list_view: {
keyFields: ["project_id"],
},
// todo: these type policies only come into play when the
// todo: these type policies only come into play when the
// query that fetches them uses an appropriate caching policy
moped_entity: {
keyFields: ["entity_id"],
Expand All @@ -80,11 +81,13 @@ const App = () => {
<LocalizationProvider dateAdapter={AdapterDateFns}>
<ThemeProvider theme={theme}>
<GlobalStyles />
<ProjectListViewQueryContext.Provider
value={{ listViewQuery, setListViewQuery }}
>
{routing}
</ProjectListViewQueryContext.Provider>
<ActivityMetrics>
<ProjectListViewQueryContext.Provider
value={{ listViewQuery, setListViewQuery }}
>
{routing}
</ProjectListViewQueryContext.Provider>
</ActivityMetrics>
</ThemeProvider>
</LocalizationProvider>
</StyledEngineProvider>
Expand Down
7 changes: 3 additions & 4 deletions moped-editor/src/auth/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,10 @@ export const initializeUserDBObject = (userObject) => {
// Then we parse the response
res.json().then((resData) => {
if (resData?.errors) {
console.error(resData.errors)
console.error(resData.errors);
}
if (resData?.data?.moped_users) {
setSessionDatabaseData(resData.data.moped_users[0]);
window.location.reload();
}
});
});
Expand Down Expand Up @@ -245,7 +244,7 @@ export const getRandomColor = () => {
export const useUser = () => {
const context = useContext(UserContext);

if (context && context.user) {
if (context && context.user && !context.user.userColor) {
context.user.userColor = getRandomColor();
}

Expand All @@ -272,7 +271,7 @@ export const getDatabaseId = (user) => {
const claims = getHasuraClaims(user);
return claims["x-hasura-user-db-id"];
} catch (e) {
console.error("getDatabaseId error ", e)
console.error("getDatabaseId error ", e);
return null;
}
};
Expand Down
28 changes: 28 additions & 0 deletions moped-editor/src/components/ActivityMetrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from "react";
import { useMutation } from "@apollo/client";
import { INSERT_USER_EVENT } from "src/queries/staff";
import { useUser } from "src/auth/user";
/**
* This wrapper calls the set_last_seen endpoint, which updates the user's
* last_seen_date timestamp based on their session ID. this action happens
* once each time this component mounts
*/
export default function ActivityMetrics({ children }) {
const { user } = useUser();
const [setLastSeen] = useMutation(INSERT_USER_EVENT, {
variables: { event_name: "app_load" },
});
const isLoggedIn = !!user;
useEffect(() => {
if (!isLoggedIn) {
return;
}
setLastSeen().catch((error) => {
console.error(
"Failed to set the last seen date for the current user.",
error
);
});
}, [setLastSeen, isLoggedIn]);
return children;
}
13 changes: 13 additions & 0 deletions moped-editor/src/queries/staff.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const GET_ALL_USERS = gql`
roles
title
is_deleted
last_seen_date
moped_workgroup {
workgroup_id
workgroup_name
Expand Down Expand Up @@ -84,3 +85,15 @@ export const UPDATE_NON_MOPED_USER = gql`
}
}
`;

/**
* This mutation insers a new moped_user_events row which will in turn trigger an
* updates to users' last_seen_date based on the user ID session variable
*/
export const INSERT_USER_EVENT = gql`
mutation InsertUserEvent($event_name: String!) {
insert_moped_user_events(objects: { event_name: $event_name }) {
affected_rows
}
}
`;
7 changes: 7 additions & 0 deletions moped-editor/src/views/staff/StaffListView.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ const staffColumns = [
// if the user has been deleted (is_deleted === True), then they are not active
valueGetter: (props) => (props.value ? "No" : "Yes"),
},
{
headerName: "Last seen",
field: "last_seen_date",
valueGetter: (props) =>
props.value ? new Date(props.value).toLocaleString() : "",
width: 200,
},
];

const StaffListView = () => {
Expand Down

0 comments on commit d790dee

Please sign in to comment.