fix(buffer): live counter
This commit is contained in:
@@ -6,8 +6,8 @@ import { getSuperJson } from '@openpanel/common';
|
|||||||
import type { IServiceEvent, Notification } from '@openpanel/db';
|
import type { IServiceEvent, Notification } from '@openpanel/db';
|
||||||
import {
|
import {
|
||||||
TABLE_NAMES,
|
TABLE_NAMES,
|
||||||
|
eventBuffer,
|
||||||
getEvents,
|
getEvents,
|
||||||
getLiveVisitors,
|
|
||||||
getProfileByIdCached,
|
getProfileByIdCached,
|
||||||
transformMinimalEvent,
|
transformMinimalEvent,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
@@ -82,20 +82,20 @@ export function wsVisitors(
|
|||||||
if (channel === 'event:received') {
|
if (channel === 'event:received') {
|
||||||
const event = getSuperJson<IServiceEvent>(message);
|
const event = getSuperJson<IServiceEvent>(message);
|
||||||
if (event?.projectId === params.projectId) {
|
if (event?.projectId === params.projectId) {
|
||||||
getLiveVisitors(params.projectId).then((count) => {
|
eventBuffer.getActiveVisitorCount(params.projectId).then((count) => {
|
||||||
connection.socket.send(String(count));
|
connection.socket.send(String(count));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const pmessage = (pattern: string, channel: string, message: string) => {
|
const pmessage = (pattern: string, channel: string, message: string) => {
|
||||||
if (!message.startsWith('live:visitors:')) {
|
if (!message.startsWith('live:visitor:')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [projectId] = getLiveEventInfo(message);
|
const [projectId] = getLiveEventInfo(message);
|
||||||
if (projectId && projectId === params.projectId) {
|
if (projectId && projectId === params.projectId) {
|
||||||
getLiveVisitors(params.projectId).then((count) => {
|
eventBuffer.getActiveVisitorCount(params.projectId).then((count) => {
|
||||||
connection.socket.send(String(count));
|
connection.socket.send(String(count));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import withSuspense from '@/hocs/with-suspense';
|
import withSuspense from '@/hocs/with-suspense';
|
||||||
|
|
||||||
import { getLiveVisitors } from '@openpanel/db';
|
import { eventBuffer } from '@openpanel/db';
|
||||||
|
|
||||||
import type { LiveCounterProps } from './live-counter';
|
import type { LiveCounterProps } from './live-counter';
|
||||||
import LiveCounter from './live-counter';
|
import LiveCounter from './live-counter';
|
||||||
|
|
||||||
async function ServerLiveCounter(props: Omit<LiveCounterProps, 'data'>) {
|
async function ServerLiveCounter(props: Omit<LiveCounterProps, 'data'>) {
|
||||||
const count = await getLiveVisitors(props.projectId);
|
const count = await eventBuffer.getActiveVisitorCount(props.projectId);
|
||||||
return <LiveCounter data={count} {...props} />;
|
return <LiveCounter data={count} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function useWS<T>(
|
|||||||
onMessage(event) {
|
onMessage(event) {
|
||||||
try {
|
try {
|
||||||
const data = getSuperJson<T>(event.data);
|
const data = getSuperJson<T>(event.data);
|
||||||
if (data) {
|
if (data !== null && data !== undefined) {
|
||||||
debouncedOnMessage(data);
|
debouncedOnMessage(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export class EventBuffer extends BaseBuffer {
|
|||||||
)
|
)
|
||||||
: 1000;
|
: 1000;
|
||||||
|
|
||||||
|
private activeVisitorsExpiration = 60 * 5; // 5 minutes
|
||||||
|
|
||||||
private sessionEvents = ['screen_view', 'session_end'];
|
private sessionEvents = ['screen_view', 'session_end'];
|
||||||
|
|
||||||
// LIST - Stores events without sessions
|
// LIST - Stores events without sessions
|
||||||
@@ -246,8 +248,11 @@ return "OK"
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.profile_id) {
|
if (event.profile_id) {
|
||||||
multi.sadd(`live:visitors:${event.project_id}`, event.profile_id);
|
this.incrementActiveVisitorCount(
|
||||||
multi.expire(`live:visitors:${event.project_id}`, 60 * 5); // 5 minutes
|
multi,
|
||||||
|
event.project_id,
|
||||||
|
event.profile_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_multi) {
|
if (!_multi) {
|
||||||
@@ -689,4 +694,44 @@ return "OK"
|
|||||||
);
|
);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async incrementActiveVisitorCount(
|
||||||
|
multi: ReturnType<Redis['multi']>,
|
||||||
|
projectId: string,
|
||||||
|
profileId: string,
|
||||||
|
) {
|
||||||
|
// Add/update visitor with current timestamp as score
|
||||||
|
const now = Date.now();
|
||||||
|
const zsetKey = `live:visitors:${projectId}`;
|
||||||
|
return (
|
||||||
|
multi
|
||||||
|
// To keep the count
|
||||||
|
.zadd(zsetKey, now, profileId)
|
||||||
|
// To trigger the expiration listener
|
||||||
|
.set(
|
||||||
|
`live:visitor:${projectId}:${profileId}`,
|
||||||
|
'1',
|
||||||
|
'EX',
|
||||||
|
this.activeVisitorsExpiration,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getActiveVisitorCount(projectId: string): Promise<number> {
|
||||||
|
const redis = getRedisCache();
|
||||||
|
const zsetKey = `live:visitors:${projectId}`;
|
||||||
|
const cutoff = Date.now() - this.activeVisitorsExpiration * 1000;
|
||||||
|
|
||||||
|
const multi = redis.multi();
|
||||||
|
multi
|
||||||
|
.zremrangebyscore(zsetKey, '-inf', cutoff)
|
||||||
|
.zcount(zsetKey, cutoff, '+inf');
|
||||||
|
|
||||||
|
const [, count] = (await multi.exec()) as [
|
||||||
|
[Error | null, any],
|
||||||
|
[Error | null, number],
|
||||||
|
];
|
||||||
|
|
||||||
|
return count[1] || 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { BotBuffer as BotBufferPsql } from './bot-buffer-psql';
|
|
||||||
import { BotBuffer as BotBufferRedis } from './bot-buffer-redis';
|
import { BotBuffer as BotBufferRedis } from './bot-buffer-redis';
|
||||||
import { EventBuffer as EventBufferPsql } from './event-buffer-psql';
|
|
||||||
import { EventBuffer as EventBufferRedis } from './event-buffer-redis';
|
import { EventBuffer as EventBufferRedis } from './event-buffer-redis';
|
||||||
import { ProfileBuffer as ProfileBufferPsql } from './profile-buffer-psql';
|
|
||||||
import { ProfileBuffer as ProfileBufferRedis } from './profile-buffer-redis';
|
import { ProfileBuffer as ProfileBufferRedis } from './profile-buffer-redis';
|
||||||
|
|
||||||
export const eventBuffer = new EventBufferRedis();
|
export const eventBuffer = new EventBufferRedis();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { mergeDeepRight, omit, uniq } from 'ramda';
|
import { mergeDeepRight, uniq } from 'ramda';
|
||||||
import { escape } from 'sqlstring';
|
import { escape } from 'sqlstring';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { toDots } from '@openpanel/common';
|
import { toDots } from '@openpanel/common';
|
||||||
import { cacheable, getRedisCache } from '@openpanel/redis';
|
import { cacheable } from '@openpanel/redis';
|
||||||
import type { IChartEventFilter } from '@openpanel/validation';
|
import type { IChartEventFilter } from '@openpanel/validation';
|
||||||
|
|
||||||
import { botBuffer, eventBuffer } from '../buffers';
|
import { botBuffer, eventBuffer } from '../buffers';
|
||||||
@@ -234,10 +234,6 @@ export function transformMinimalEvent(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLiveVisitors(projectId: string) {
|
|
||||||
return getRedisCache().scard(`live:visitors:${projectId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEvents(
|
export async function getEvents(
|
||||||
sql: string,
|
sql: string,
|
||||||
options: GetEventsOptions = {},
|
options: GetEventsOptions = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user