1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

Count active browser sessions per user (#8736)

Show info on how many devices a user is logged in to an admin.
This commit is contained in:
Tymoteusz Czech 2024-11-13 16:49:25 +01:00 committed by GitHub
parent bcbbd5c3e6
commit 60fb647489
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 80 additions and 0 deletions

View File

@ -108,6 +108,18 @@ export default class SessionStore implements ISessionStore {
expired: row.expired,
};
}
async getSessionsCount(): Promise<{ userId: number; count: number }[]> {
const rows = await this.db(TABLE)
.select(this.db.raw("sess->'user'->>'id' AS user_id"))
.count('* as count')
.groupBy('user_id');
return rows.map((row) => ({
userId: Number(row.user_id),
count: Number(row.count),
}));
}
}
module.exports = SessionStore;

View File

@ -99,6 +99,12 @@ export const userSchema = {
nullable: true,
example: '01HTMEXAMPLESCIMID7SWWGHN6',
},
activeSessions: {
description: 'Count of active browser sessions for this user',
type: 'integer',
nullable: true,
example: 2,
},
},
components: {},
} as const;

View File

@ -60,6 +60,14 @@ export default class SessionService {
}: Pick<ISession, 'sid' | 'sess'>): Promise<ISession> {
return this.sessionStore.insertSession({ sid, sess });
}
async getSessionsCount() {
return Object.fromEntries(
(await this.sessionStore.getSessionsCount()).map(
({ userId, count }) => [userId, count],
),
);
}
}
module.exports = SessionService;

View File

@ -209,6 +209,15 @@ class UserService {
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
return { ...u, rootRole: roleId };
});
if (this.flagResolver.isEnabled('showUserDeviceCount')) {
const sessionCounts = await this.sessionService.getSessionsCount();
const usersWithSessionCounts = usersWithRootRole.map((u) => ({
...u,
activeSessions: sessionCounts[u.id] || 0,
}));
return usersWithSessionCounts;
}
return usersWithRootRole;
}

View File

@ -59,6 +59,7 @@ export type IFlagKey =
| 'enterprise-payg'
| 'simplifyProjectOverview'
| 'flagOverviewRedesign'
| 'showUserDeviceCount'
| 'deleteStaleUserSessions';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -283,6 +284,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_FLAG_OVERVIEW_REDESIGN,
false,
),
showUserDeviceCount: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_SHOW_USER_DEVICE_COUNT,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -12,4 +12,5 @@ export interface ISessionStore extends Store<ISession, string> {
getSessionsForUser(userId: number): Promise<ISession[]>;
deleteSessionsForUser(userId: number): Promise<void>;
insertSession(data: Omit<ISession, 'createdAt'>): Promise<ISession>;
getSessionsCount(): Promise<{ userId: number; count: number }[]>;
}

View File

@ -55,6 +55,7 @@ process.nextTick(async () => {
webhookDomainLogging: true,
releasePlans: false,
simplifyProjectOverview: true,
showUserDeviceCount: true,
flagOverviewRedesign: true,
},
},

View File

@ -38,6 +38,7 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
showUserDeviceCount: true,
},
},
});
@ -428,3 +429,36 @@ test('creates user with email md5 hash', async () => {
expect(user.email_hash).toBe(expectedHash);
});
test('should return number of sessions per user', async () => {
const user = await userStore.insert({ email: 'tester@example.com' });
await sessionStore.insertSession({
sid: '1',
sess: { user: { id: user.id } },
});
await sessionStore.insertSession({
sid: '2',
sess: { user: { id: user.id } },
});
const user2 = await userStore.insert({ email: 'tester2@example.com' });
await sessionStore.insertSession({
sid: '3',
sess: { user: { id: user2.id } },
});
const response = await app.request.get(`/api/admin/user-admin`).expect(200);
expect(response.body).toMatchObject({
users: expect.arrayContaining([
expect.objectContaining({
email: 'tester@example.com',
activeSessions: 2,
}),
expect.objectContaining({
email: 'tester2@example.com',
activeSessions: 1,
}),
]),
});
});

View File

@ -52,4 +52,8 @@ export default class FakeSessionStore implements ISessionStore {
this.sessions.push(session);
return session;
}
async getSessionsCount(): Promise<{ userId: number; count: number }[]> {
return [];
}
}