Analytics revise (#91)

Analytics revise
This commit is contained in:
Lin, Hung-Shin authored and GitHub committed 2023-12-21 18:13:53 +08:00
commit 4a4ce11bcc
24 files changed
+391 -148

No files matched your search

+2 -1
View File
@@ -13,7 +13,8 @@
"@linux-odyssey/models": "*",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"express-handlebars": "^7.1.2"
"express-handlebars": "^7.1.2",
"mongoose": "^8.0.3"
},
"devDependencies": {
"nodemon": "^3.0.1"
View File
Whitespace-only changes.
+18 -3
View File
@@ -1,9 +1,24 @@
/* eslint-disable object-shorthand */
import Pagination from '../models/pagination.js'
import { errorCommands } from '../models/commands.js'
// eslint-disable-next-line import/prefer-default-export
export async function commandListController(req, res) {
const payload = {
errorCommands: await errorCommands(),
const { nextKey, order } = req.query
const itemsPerPage = 100
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const payload = await errorCommands(pagination)
const newNextKey = payload[payload.length - 1]?._id
res.render('commands', {
errorCommands: payload,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching data')
}
res.render('commands', payload)
}
+13 -4
View File
@@ -1,15 +1,24 @@
import { userCount } from '../models/users.js'
import { sessionCount, sessionCompleted } from '../models/sessions.js'
import { questList } from '../models/quests.js'
// eslint-disable-next-line import/prefer-default-export
export async function homeController(req, res) {
const quests = await questList()
const [totalQuests, totalCompleted] = quests.reduce(
([total, completed], quest) => [
total + quest.started,
completed + quest.completed,
],
[0, 0]
)
const payload = {
title: 'Linux Odyssey Admin',
userCount: await userCount(),
sessionCount: await sessionCount(),
sessionCompleted: await sessionCompleted(),
totalQuests,
totalCompleted,
}
payload.completedRate =
Math.round((payload.sessionCompleted / payload.sessionCount) * 10000) / 100
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
res.render('home', payload)
}
@@ -0,0 +1,10 @@
import { questList } from '../models/quests.js'
// eslint-disable-next-line import/prefer-default-export
export async function questListController(req, res) {
const quests = await questList()
quests.forEach((quest) => {
quest.failed = quest.started - quest.completed
})
res.render('quests', { quests })
}
+18 -2
View File
@@ -1,8 +1,24 @@
import Pagination from '../models/pagination.js'
import { sessionList, sessionDetail } from '../models/sessions.js'
export async function sessionListController(req, res) {
const sessions = await sessionList()
res.render('sessions', { sessions })
const { nextKey, order } = req.query
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const sessions = await sessionList(pagination)
const newNextKey = sessions[sessions.length - 1]?._id
res.render('sessions', {
sessions,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.log(error)
res.status(500).send('Error fetching session data')
}
}
export async function sessionDetailController(req, res) {
+18 -2
View File
@@ -1,6 +1,22 @@
import Pagination from '../models/pagination.js'
import { userList } from '../models/users.js'
// eslint-disable-next-line import/prefer-default-export
export async function userListController(req, res) {
res.render('users', { users: await userList() })
const { nextKey, order } = req.query
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const users = await userList(pagination)
const newNextKey = users[users.length - 1]?._id
res.render('users', {
users,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
+36 -14
View File
@@ -1,22 +1,44 @@
import { Command } from '@linux-odyssey/models'
// eslint-disable-next-line import/prefer-default-export
export async function errorCommands() {
const commands = await Command.find({
$and: [
{ error: { $exists: true } },
{ error: { $ne: null } },
{ error: { $ne: '' } },
],
})
.populate({
path: 'session',
populate: { path: 'user' },
})
.exec()
return commands.map(({ command, error, createdAt, session }) => {
export async function errorCommands(pagination) {
const commands = await Command.aggregate([
pagination.match('_id'),
{
$match: {
$and: [
{ error: { $exists: true } },
{ error: { $ne: null } },
{ error: { $ne: '' } },
],
},
},
{
$lookup: {
from: 'sessions', // 替換成 'session' 對應的集合名稱
localField: 'session', // 替換成 'session' 對應的字段名稱
foreignField: '_id',
as: 'session',
},
},
{ $unwind: '$session' },
{
$lookup: {
from: 'users', // 替換成 'user' 對應的集合名稱
localField: 'session.user', // 替換成 'user' 對應的字段名稱
foreignField: '_id',
as: 'session.user',
},
},
{ $unwind: '$session.user' },
pagination.sort('_id'),
pagination.limit(),
])
return commands.map(({ _id, command, error, createdAt, session }) => {
const { user, quest } = session
return {
_id,
command: command.slice(0, 20),
error: error.slice(0, 50),
createdAt: createdAt?.toLocaleString(),
@@ -0,0 +1,74 @@
/* eslint-disable lines-between-class-members */
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line max-classes-per-file
import mongoose from 'mongoose'
export const DESC = 'desc'
export const ASC = 'asc'
class Asc {
getOperator() {
return '$gt'
}
getOrder() {
return 1
}
getName() {
return ASC
}
}
class Desc {
getOperator() {
return '$lt'
}
getOrder() {
return -1
}
getName() {
return DESC
}
}
export default class Pagination {
nextKey = null
itemsPerPage = 50
order = null
constructor(order, itemsPerPage = 50, nextKey = null) {
this.itemsPerPage = itemsPerPage
if (!order || order === DESC) {
this.order = new Desc()
} else if (order === ASC) {
this.order = new Asc()
} else {
throw new Error('order must be asc or desc')
}
if (nextKey) {
this.nextKey = new mongoose.Types.ObjectId(nextKey)
}
}
match(field) {
const stage = this.nextKey
? { [field]: { [this.order.getOperator()]: this.nextKey } }
: {}
return { $match: stage }
}
sort(field) {
return { $sort: { [field]: this.order.getOrder() } }
}
limit() {
return { $limit: this.itemsPerPage }
}
getOrder() {
return this.order.getName()
}
}
+59
View File
@@ -0,0 +1,59 @@
/* eslint-disable object-shorthand */
/* eslint-disable func-names */
import { UserProfile } from '@linux-odyssey/models'
export function questList() {
return UserProfile.aggregate([
{
$project: {
progress: {
$objectToArray: '$progress',
},
},
},
{
$unwind: '$progress',
},
{
$group: {
_id: '$progress.v.quest',
started: {
$sum: 1,
},
completed: {
$sum: {
$cond: {
if: { $eq: ['$progress.v.completed', true] },
then: 1,
else: 0,
},
},
},
},
},
{
$project: {
_id: 1,
started: 1,
completed: 1,
completedRate: {
$cond: {
if: { $eq: ['$started', 0] },
then: 0,
else: {
$round: [
{ $multiply: [{ $divide: ['$completed', '$started'] }, 100] },
0,
],
},
},
},
},
},
{
$sort: {
completed: -1,
},
},
])
}
+5 -24
View File
@@ -1,28 +1,5 @@
import { Command, Session } from '@linux-odyssey/models'
export function sessionCount() {
return Session.find().count()
}
export function sessionCompleted() {
return Session.find({ status: 'finished' }).count()
}
export function averageTimeUsage() {
return Session.aggregate([
{
$group: {
_id: '$quest',
average: {
$avg: {
$subtract: ['$finishedAt', '$createdAt'],
},
},
},
},
])
}
function formatTime(time) {
const seconds = Math.floor(time / 1000)
const minutes = Math.floor(seconds / 60)
@@ -30,8 +7,9 @@ function formatTime(time) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
}
export async function sessionList() {
export async function sessionList(pagination) {
const sessions = await Session.aggregate([
pagination.match('_id'),
{
$lookup: {
from: 'users',
@@ -49,7 +27,10 @@ export async function sessionList() {
as: 'commands',
},
},
pagination.sort('_id'),
pagination.limit(),
])
return sessions.map(
({
_id,
+7 -3
View File
@@ -11,8 +11,9 @@ export function userCount() {
return User.find().count()
}
export async function userList() {
export async function userList(pagination) {
const users = await Session.aggregate([
pagination.match('user'),
{
$group: {
_id: '$user',
@@ -29,11 +30,14 @@ export async function userList() {
},
},
{ $unwind: '$userData' },
{ $sort: { 'userData.createdAt': 1 } },
pagination.sort('userData._id'),
pagination.limit(),
])
return users.map(({ userData, count, lastActivityAt }) => {
const { username, email, createdAt } = userData
const { _id, username, email, createdAt } = userData
return {
_id,
username,
email,
createdAt: createdAt?.toLocaleString(),
+2
View File
@@ -2,6 +2,7 @@ import { Router } from 'express'
import { homeController } from './controllers/index.js'
import { userListController } from './controllers/users.js'
import { questListController } from './controllers/quests.js'
import { commandListController } from './controllers/commands.js'
import {
sessionDetailController,
@@ -12,6 +13,7 @@ const router = Router()
router.get('/', homeController)
router.get('/users', userListController)
router.get('/quests', questListController)
router.get('/commands', commandListController)
router.get('/sessions', sessionListController)
router.get('/sessions/:id', sessionDetailController)
@@ -0,0 +1,48 @@
import mongoose from 'mongoose'
import Pagination, { ASC, DESC } from '../src/models/pagination.js'
describe('Pagination', () => {
it('should failed when not provide order', () => {
expect(new Pagination('').getOrder()).toBe(DESC)
expect(() => new Pagination('Hahaha')).toThrow()
})
it('should return correct limit', () => {
const pagination = new Pagination(DESC, 10)
expect(pagination.limit()).toMatchObject({ $limit: 10 })
})
it('desc should return correct order', async () => {
const pagination = new Pagination(DESC, 10)
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: -1 } })
expect(pagination.sort('time')).toMatchObject({ $sort: { time: -1 } })
})
it('asc should return correct order', async () => {
const pagination = new Pagination(ASC, 10)
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: 1 } })
expect(pagination.sort('time')).toMatchObject({ $sort: { time: 1 } })
})
it('should return correct match stage for empty nextKey', async () => {
const pagination = new Pagination(DESC, 10)
expect(pagination.match('_id')).toMatchObject({ $match: {} })
})
it('should return correct match stage for empty nextKey', async () => {
const nextKeyStr = '656c7fea28a4c087bfcefe12'
const pagination = new Pagination(DESC, 10, nextKeyStr)
const nextKey = new mongoose.Types.ObjectId(nextKeyStr)
expect(pagination.match('_id')).toMatchObject({
$match: {
_id: {
$lt: nextKey,
},
},
})
})
})
@@ -1,6 +1,10 @@
<h1>Commands</h1>
<div>
<h2>Error commands</h2>
<div class='pagination'>
<a href='/commands?order=asc'>Ascending</a>
<a href='/commands?order=desc'>Descending</a>
</div>
<table>
<tr>
<th>Command</th>
@@ -21,4 +25,7 @@
</tr>
{{/each}}
</table>
<div class='pagination'>
<a href='/commands?nextKey={{nextKey}}&order={{order}}'>Next</a>
</div>
</div>
+5 -5
View File
@@ -1,18 +1,18 @@
<h1>Welcome to {{title}}</h1>
<div>
<h2>Users</h2>
<h2>Overview</h2>
<table>
<tr>
<th>Registered Users</th>
<td>{{userCount}}</td>
</tr>
<tr>
<th>Session Count</th>
<td>{{sessionCount}}</td>
<th>Total Started Quests</th>
<td>{{totalQuests}}</td>
</tr>
<tr>
<th>Completed Session</th>
<td>{{sessionCompleted}}</td>
<th>Completed Quests</th>
<td>{{totalCompleted}}</td>
</tr>
<tr>
<th>Completed Rate</th>
@@ -8,6 +8,7 @@
<ul>
<li><a href='/'>Home</a></li>
<li><a href='/users'>Users</a></li>
<li><a href='/quests'>Quests</a></li>
<li><a href='/commands'>Commands</a></li>
<li><a href='/sessions'>Sessions</a></li>
</ul>
@@ -0,0 +1,20 @@
<h1>Quests</h1>
<div>
<table>
<tr>
<th>Id</th>
<th>Started</th>
<th>Completed</th>
<th>Failed</th>
<th>Completed Rate</th>
</tr>
{{#each quests}}
<tr>
<td>{{_id}}</td>
<td>{{started}}</td>
<td>{{completed}}</td>
<td>{{failed}}</td>
<td>{{completedRate}} %</td>
</tr>
{{/each}}
</table>
@@ -36,20 +36,20 @@
<table>
<tr>
<th>Command</th>
<th>Stage</th>
<th>PWD</th>
<th>Input</th>
<th>Output</th>
<th>Error</th>
<th>Stage</th>
<th>Created At</th>
<th>Input Time</th>
</tr>
{{#each commands}}
<tr>
<td>{{command}}</td>
<td>{{stage}}</td>
<td>{{pwd}}</td>
<td>{{command}}</td>
<td>{{output}}</td>
<td>{{error}}</td>
<td>{{stage}}</td>
<td>{{createdAt}}</td>
</tr>
{{/each}}
+9 -2
View File
@@ -1,27 +1,34 @@
<h1>Sessions</h1>
<div class='pagination'>
<a href='/sessions?order=asc'>Ascending</a>
<a href='/sessions?order=desc'>Descending</a>
</div>
<div>
<table>
<tr>
<th>User</th>
<th>Quest</th>
<th></th>
<th>Status</th>
<th>Created At</th>
<th>Finished At</th>
<th>Used Time</th>
<th>Command Count</th>
<th>Session</th>
</tr>
{{#each sessions}}
<tr>
<td>{{user}}</td>
<td>{{quest}}</td>
<td><a href='/sessions/{{_id}}'>Detail</a></td>
<td>{{status}}</td>
<td>{{createdAt}}</td>
<td>{{finishedAt}}</td>
<td>{{usedTime}}</td>
<td>{{commandCount}}</td>
<td><a href='/sessions/{{_id}}'>Detail</a></td>
</tr>
{{/each}}
</table>
</div>
<div class='pagination'>
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'>Next</a>
</div>
+16 -9
View File
@@ -1,23 +1,30 @@
<h1>Users List</h1>
<div class='pagination'>
<a href='/users?order=asc'>Ascending</a>
<a href='/users?order=desc'>Descending</a>
</div>
<div>
<table>
<tr>
<th>Username</th>
<th>Email</th>
<th>Registration Method</th>
<th>Registration Time</th>
<th>Sessions Count</th>
<th>Last Activity</th>
<th>Sessions Count</th>
<th>Email</th>
<th>Registration Time</th>
<th>Registration Method</th>
</tr>
{{#each users}}
<tr>
<td>{{username}}</td>
<td>{{email}}</td>
<td>{{method}}</td>
<td>{{createdAt}}</td>
<td>{{sessionCount}}</td>
<td href='/users/{{_id}}'>{{username}}</td>
<td>{{lastActivityAt}}</td>
<td>{{sessionCount}}</td>
<td>{{email}}</td>
<td>{{createdAt}}</td>
<td>{{method}}</td>
</tr>
{{/each}}
</table>
</div>
<div class='pagination'>
<a href='/users?nextKey={{nextKey}}&order={{order}}'>Next</a>
</div>
+1 -1
View File
@@ -6,6 +6,6 @@
"license": "GPL",
"exports": "./index.js",
"dependencies": {
"mongoose": "^7.6.0"
"mongoose": "^8.0.3"
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.1",
"minimist": "^1.2.8",
"mongoose": "^7.4.0",
"mongoose": "^8.0.3",
"node-schedule": "^2.1.1",
"passport": "^0.6.0",
"passport-github2": "^0.1.12",
+16 -72
View File
@@ -1756,15 +1756,10 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
bson@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6"
integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==
bson@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.0.tgz#a419cc69f368d2def3b8b22ea03ed1c9be40e53f"
integrity sha512-B+QB4YmDx9RStKv8LLSl/aVIEV3nYJc3cJNNTK2Cd1TL+7P+cNpw9mAPeCgc5K+j01Dv6sxUzcITXDx7ZU3F0w==
bson@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-6.2.0.tgz#4b6acafc266ba18eeee111373c2699304a9ba0a3"
integrity sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==
buffer-crc32@~0.2.3:
version "0.2.13"
@@ -3668,11 +3663,6 @@ internal-slot@^1.0.5:
has "^1.0.3"
side-channel "^1.0.4"
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@@ -4833,49 +4823,23 @@ mongodb-connection-string-url@^2.6.0:
"@types/whatwg-url" "^8.2.1"
whatwg-url "^11.0.0"
mongodb@5.7.0:
version "5.7.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.7.0.tgz#e16d2fcdfd9f8503ec2d88288392dc3235bb3ecc"
integrity sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==
mongodb@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.2.0.tgz#2c9dcb3eeaf528ed850e94b3df392de6c6b0d7ab"
integrity sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==
dependencies:
bson "^5.4.0"
mongodb-connection-string-url "^2.6.0"
socks "^2.7.1"
optionalDependencies:
saslprep "^1.0.3"
mongodb@5.9.0:
version "5.9.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.9.0.tgz#5a22065fa8cfaf1d58bf2e3c451cd2c4bfa983a2"
integrity sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==
dependencies:
bson "^5.5.0"
mongodb-connection-string-url "^2.6.0"
socks "^2.7.1"
optionalDependencies:
"@mongodb-js/saslprep" "^1.1.0"
bson "^6.2.0"
mongodb-connection-string-url "^2.6.0"
mongoose@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.4.0.tgz#3726a8dea0c9e2d54651845cc84c813a6507906a"
integrity sha512-oHE1eqodfKzugXRlQxpo+msIea7jPcRoayDuEMr50+bYwM/juA5f+1stjkWlXcg6vo1PdJFVA6DGaKOPLuG5mA==
mongoose@^8.0.3:
version "8.0.3"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-8.0.3.tgz#f3708ba0f40b6dec8f5f1a5d40abd69a80207114"
integrity sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==
dependencies:
bson "^5.4.0"
bson "^6.2.0"
kareem "2.5.1"
mongodb "5.7.0"
mpath "0.9.0"
mquery "5.0.0"
ms "2.1.3"
sift "16.0.1"
mongoose@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.6.0.tgz#3037b754ccdf168385e52d4bcabeb7f5c3b17a5f"
integrity sha512-ztQ12rm0BQN5i7LB6xhWX4l9a9w2aa3jEwa/mM2vAutYJRyAwOzcusvKJBULMzFHyUDBOVW15grisexypgMIWA==
dependencies:
bson "^5.4.0"
kareem "2.5.1"
mongodb "5.9.0"
mongodb "6.2.0"
mpath "0.9.0"
mquery "5.0.0"
ms "2.1.3"
@@ -5758,13 +5722,6 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
saslprep@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
dependencies:
sparse-bitfield "^3.0.3"
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@@ -5913,11 +5870,6 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
socket.io-adapter@~2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
@@ -5956,14 +5908,6 @@ socket.io@^4.7.1:
socket.io-adapter "~2.5.2"
socket.io-parser "~4.2.4"
socks@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
dependencies:
ip "^2.0.0"
smart-buffer "^4.2.0"
sorted-array-functions@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"