Skip to content

Commit

Permalink
add activity queries
Browse files Browse the repository at this point in the history
  • Loading branch information
rotorsoft committed Oct 10, 2024
1 parent ece7ba9 commit 5c30a68
Show file tree
Hide file tree
Showing 26 changed files with 275 additions and 236 deletions.
4 changes: 2 additions & 2 deletions common_knowledge/Caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ _"There are only two hard things in Computer Science: cache invalidation and nam
- Choosing a right TTL could be very important, as app data is highly transactional

- **Caching Namespace:**
- Global - eg. /api/viewGlobalActivity
- User Specific - eg. /api/viewUserActivity or /api/status
- Global - eg. /api/internal/trpc/feed.GetGlobalActivity
- User Specific - eg. /api/internal/trpc/feed.GetUserActivity or /api/status

- **Hybrid Request Handler** -> Fetch followed by tracking of user activity
eg. get chain data & record current chain selected by user - these kind of request handler can only utilize sequelize result caching
Expand Down
2 changes: 1 addition & 1 deletion common_knowledge/Performance-Benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ _See also full [Datadog](./Datadog.md) entry._

We've implemented two new performance dashboards to monitor and track improvements in latency and call volume over time.

These dashboards serve a key role in prioritizing improvements by highlighting high call volume and slow endpoints. They are instrumental in monitoring the effect of improvements over time, as shown by specific examples corresponding to PRs like `getAddressProfile` larger batches and performance enhancements in backend API calls such as `/viewUserActivity` and `/status`. Additionally, they can effectively detect abnormal spikes in latency and call volume through real-time metrics reported to Datadog.
These dashboards serve a key role in prioritizing improvements by highlighting high call volume and slow endpoints. They are instrumental in monitoring the effect of improvements over time, as shown by specific examples corresponding to PRs like `getAddressProfile` larger batches and performance enhancements in backend API calls such as `/feed.GetUserActivity` and `/status`. Additionally, they can effectively detect abnormal spikes in latency and call volume through real-time metrics reported to Datadog.

## Change Log

Expand Down
7 changes: 3 additions & 4 deletions libs/model/src/feed/GetGlobalActivity.query.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Query } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { models } from '../database';
import { GlobalActivityCache } from '../globalActivityCache';

export function GetGlobalActivity(): Query<typeof schemas.ThreadFeed> {
export function GetGlobalActivity(): Query<typeof schemas.ActivityFeed> {
return {
...schemas.ThreadFeed,
...schemas.ActivityFeed,
auth: [],
secure: false,
body: async () => {
return await GlobalActivityCache.getInstance(models).getGlobalActivity();
return await GlobalActivityCache.getInstance().getGlobalActivity();
},
};
}
12 changes: 12 additions & 0 deletions libs/model/src/feed/GetUserActivity.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Query } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { getUserActivityFeed } from '../globalActivityCache';

export function GetUserActivity(): Query<typeof schemas.ActivityFeed> {
return {
...schemas.ActivityFeed,
auth: [],
secure: false,
body: async ({ actor }) => await getUserActivityFeed(actor.user.id),
};
}
2 changes: 1 addition & 1 deletion libs/model/src/feed/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './GetGlobalActivity.query';
export * from './GetUserFeed.query';
export * from './GetUserActivity.query';
43 changes: 22 additions & 21 deletions libs/model/src/globalActivityCache.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { CacheNamespaces, cache, logger } from '@hicommonwealth/core';
import { ActivityFeedRecord } from '@hicommonwealth/schemas';
import { QueryTypes } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
import { DB } from './models/index';
import { z } from 'zod';
import { models } from './database';

export async function getActivityFeed(models: DB, id = 0) {
export async function getUserActivityFeed(user_id?: number) {
/**
* Last 50 updated threads and their comments
*/
const query = `
WITH
user_communities AS (SELECT DISTINCT community_id FROM "Addresses" WHERE user_id = :id),
user_communities AS (SELECT DISTINCT community_id FROM "Addresses" WHERE user_id = :user_id),
top_threads AS (
SELECT T.*
FROM "Threads" T
${
id > 0
user_id
? 'JOIN user_communities UC ON UC.community_id = T.community_id'
: ''
}
Expand Down Expand Up @@ -54,7 +56,7 @@ export async function getActivityFeed(models: DB, id = 0) {
JOIN "Addresses" A ON A.id = T.address_id AND A.community_id = T.community_id
JOIN "Users" U ON U.id = A.user_id
JOIN "Topics" Tp ON Tp.id = T.topic_id
${id > 0 ? 'WHERE U.id != :id' : ''}),
${user_id ? 'WHERE U.id != :user_id' : ''}),
recent_comments AS ( -- get the recent comments data associated with the thread
SELECT
C.thread_id as thread_id,
Expand All @@ -70,7 +72,7 @@ export async function getActivityFeed(models: DB, id = 0) {
'profile_name', U.profile->>'name',
'profile_avatar_url', U.profile->>'avatar_url',
'user_id', U.id
))) as "recentComments"
))) as recent_comments
FROM (
Select tempC.*
FROM "Comments" tempC
Expand All @@ -84,23 +86,23 @@ export async function getActivityFeed(models: DB, id = 0) {
GROUP BY C.thread_id
)
SELECT
RTS."thread" as thread,
RC."recentComments" as recentComments
RTS.thread,
RC.recent_comments
FROM
ranked_threads RTS
LEFT JOIN recent_comments RC ON RTS.thread_id = RC.thread_id
ORDER BY
RTS.activity_rank_date DESC NULLS LAST;
`;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const threads: any = await models.sequelize.query(query, {
type: QueryTypes.SELECT,
raw: true,
replacements: { id },
});

return threads;
return await models.sequelize.query<z.infer<typeof ActivityFeedRecord>>(
query,
{
type: QueryTypes.SELECT,
raw: true,
replacements: { user_id },
},
);
}

const log = logger(import.meta);
Expand All @@ -111,13 +113,12 @@ export class GlobalActivityCache {
private static _instance: GlobalActivityCache;

constructor(
private _models: DB,
private _cacheTTL: number = 60 * 5, // cache TTL in seconds
) {}

static getInstance(models: DB, cacheTTL?: number): GlobalActivityCache {
static getInstance(cacheTTL?: number): GlobalActivityCache {
if (!GlobalActivityCache._instance) {
GlobalActivityCache._instance = new GlobalActivityCache(models, cacheTTL);
GlobalActivityCache._instance = new GlobalActivityCache(cacheTTL);
}
return GlobalActivityCache._instance;
}
Expand All @@ -139,7 +140,7 @@ export class GlobalActivityCache {
const msg = 'Failed to fetch global activity from Redis';
log.error(msg);
}
return await getActivityFeed(this._models);
return await getUserActivityFeed();
}
return JSON.parse(activity);
}
Expand Down Expand Up @@ -193,7 +194,7 @@ export class GlobalActivityCache {
return;
}

const activity = await getActivityFeed(this._models);
const activity = await getUserActivityFeed();
const result = await cache().setKey(
CacheNamespaces.Activity_Cache,
this._cacheKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { InvalidInput, Query, ServerError } from '@hicommonwealth/core';
import { InvalidInput, Query } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import moment from 'moment';
import { QueryTypes } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';

export function GetUserFeed(): Query<typeof schemas.GetUserFeed> {
export function GetThreads(): Query<typeof schemas.GetThreads> {
return {
...schemas.GetUserFeed,
...schemas.GetThreads,
auth: [],
secure: false,
body: async ({ payload }) => {
Expand Down Expand Up @@ -250,23 +250,17 @@ export function GetUserFeed(): Query<typeof schemas.GetUserFeed> {
},
});

try {
const [threads, numVotingThreads] = await Promise.all([
responseThreadsQuery,
numVotingThreadsQuery,
]);
console.log(threads);
const [threads, numVotingThreads] = await Promise.all([
responseThreadsQuery,
numVotingThreadsQuery,
]);

return {
limit: replacements.limit,
page: replacements.page,
// data params
threads,
numVotingThreads,
};
} catch (e) {
throw new ServerError('Could not fetch threads', e as Error);
}
return {
limit: replacements.limit,
page: replacements.page,
threads,
numVotingThreads,
};
},
};
}
1 change: 1 addition & 0 deletions libs/model/src/thread/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './CreateThread.command';
export * from './CreateThreadReaction.command';
export * from './DeleteThread.command';
export * from './GetThread.query';
export * from './GetThreads.query';
export * from './UpdateThread.command';
117 changes: 48 additions & 69 deletions libs/schemas/src/queries/feed.schemas.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,56 @@
import { z } from 'zod';
import { Thread } from '../entities';
import { PG_INT } from '../utils';
import { DiscordMetaSchema, PG_INT } from '../utils';

export const ThreadFeedRecord = z.object({
thread_id: PG_INT,
last_activity: z.coerce.string(),
notification_data: z.string(),
category_id: z.string(),
comment_count: PG_INT,
commenters: z.array(
z.object({
Addresses: z.array(
z.object({
id: PG_INT,
address: z.string(),
community_id: z.string(),
}),
),
}),
),
export const ActivityThread = z.object({
id: PG_INT,
community_id: z.string(),
body: z.string(),
title: z.string(),
numberOfComments: PG_INT,
created_at: z.string().nullish(),
updated_at: z.string().nullish(),
deleted_at: z.string().nullish(),
locked_at: z.string().nullish(),
kind: z.string(),
stage: z.string(),
archived_at: z.string().nullish(),
read_only: z.boolean(),
has_poll: z.boolean().nullish(),
marked_as_spam_at: z.string().nullish(),
discord_meta: DiscordMetaSchema.nullish(),
profile_name: z.string().nullish(),
profile_avatar: z.string().nullish(),
user_id: PG_INT,
user_address: z.string(),
topic: z.object({
id: PG_INT,
name: z.string(),
description: z.string(),
}),
});

export const ActivityComment = z.object({
id: PG_INT,
address: z.string(),
text: z.string(),
created_at: z.string(),
updated_at: z.string().nullish(),
deleted_at: z.string().nullish(),
marked_as_spam_at: z.string().nullish(),
discord_meta: DiscordMetaSchema.nullish(),
profile_name: z.string().nullish(),
profile_avatar_url: z.string().nullish(),
user_id: z.number().nullish(),
});

export const ThreadFeed = {
export const ActivityFeedRecord = z.object({
thread: ActivityThread,
recent_comments: z.array(ActivityComment).nullish(),
});

export const ActivityFeed = {
input: z.object({}),
output: z.array(ThreadFeedRecord),
output: z.array(ActivityFeedRecord),
};

export const ChainFeedRecord = z.object({
Expand All @@ -40,51 +67,3 @@ export const ChainFeed = {
input: z.object({}),
output: z.array(ChainFeedRecord),
};

export const MappedReaction = z.object({
id: z.number(),
type: z.literal('like'),
address: z.string(),
updated_at: z.date(),
voting_weight: z.number(),
profile_name: z.string().optional(),
avatar_url: z.string().optional(),
last_active: z.date().optional(),
});

export const MappedThread = Thread.extend({
associatedReactions: z.array(MappedReaction),
});

export const GetUserFeedStatus = z.enum(['active', 'pastWinners', 'all']);
export const GetUserFeedOrderBy = z.enum([
'newest',
'oldest',
'mostLikes',
'mostComments',
'latestActivity',
]);

export const GetUserFeed = {
input: z.object({
community_id: z.string(),
page: z.number().optional(),
limit: z.number().optional(),
stage: z.string().optional(),
topic_id: PG_INT.optional(),
includePinnedThreads: z.boolean().optional(),
order_by: GetUserFeedOrderBy.optional(),
from_date: z.string().optional(),
to_date: z.string().optional(),
archived: z.boolean().optional(),
contestAddress: z.string().optional(),
status: GetUserFeedStatus.optional(),
withXRecentComments: z.number().optional(),
}),
output: z.object({
page: z.number(),
limit: z.number(),
numVotingThreads: z.number(),
threads: z.array(MappedThread),
}),
};
Loading

0 comments on commit 5c30a68

Please sign in to comment.