Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ed2feda
Create odd-hounds-develop.md
ahmed-n-abdeltwab Jul 2, 2025
763c493
feat: adds statistics API with detailed schemas
ahmed-n-abdeltwab Jul 2, 2025
2efd333
refactor: copy from the rest-typings in packages and typing to the bo…
ahmed-n-abdeltwab Jul 2, 2025
969497e
feat: add statistics endpoints and extend API typings
ahmed-n-abdeltwab Jul 2, 2025
7985303
feat: add statistics list endpoints and extend API typings
ahmed-n-abdeltwab Jul 2, 2025
5ce85e0
feat: add telemetry endpoints to statistics type and extend API typings
ahmed-n-abdeltwab Jul 2, 2025
3ea3eb1
refactor: remove unused Statistics import from index.ts
ahmed-n-abdeltwab Jul 2, 2025
fa97d5e
refactor: remove statistics type definitions and related schemas
ahmed-n-abdeltwab Jul 2, 2025
231f963
fix: update Statistics and StatisticsList type definitions
ahmed-n-abdeltwab Jul 2, 2025
f67c0e4
fix: add statistics endpoint schemas to allow null values for optiona…
ahmed-n-abdeltwab Jul 5, 2025
3831bba
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Jul 5, 2025
911cc0b
fix: update statistics endpoint schemas to use 'oneOf' instead of 'an…
ahmed-n-abdeltwab Jul 5, 2025
80b47f5
fix: use the IControl instead to fix the missing 'hash' pros
ahmed-n-abdeltwab Jul 6, 2025
c9e337e
fix: add missing statistics schema
ahmed-n-abdeltwab Jul 6, 2025
b8bf637
fix: update statistics endpoint schemas to use 'anyOf' instead of 'on…
ahmed-n-abdeltwab Jul 6, 2025
db0a4f2
fix: replace integer with number
ahmed-n-abdeltwab Jul 8, 2025
0b19d0e
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Jul 8, 2025
e339c53
chore: add debug logs
ahmed-n-abdeltwab Jul 9, 2025
7bbe36b
fix: the stats schema by make it more details
ahmed-n-abdeltwab Jul 9, 2025
e0219a3
fix: add miss pros 'id' to omnichannelSources
ahmed-n-abdeltwab Jul 9, 2025
07c8556
fix: add missing [] to omnichannelSources
ahmed-n-abdeltwab Jul 9, 2025
cf5b142
fix: add debug logs
ahmed-n-abdeltwab Jul 9, 2025
7c5367e
fix: add debug logs
ahmed-n-abdeltwab Jul 9, 2025
091d0d3
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Jul 9, 2025
c8102e4
fix: add more debug logs
ahmed-n-abdeltwab Jul 10, 2025
710364e
fix: add missing pros and clean the schemas
ahmed-n-abdeltwab Jul 10, 2025
2dbcac6
chore: add TODO for a duplicated pros
ahmed-n-abdeltwab Jul 10, 2025
3a8f6b5
chore: add logs for debug
ahmed-n-abdeltwab Jul 10, 2025
75bb683
chore: remove the debug logs and add clear the message TODO
ahmed-n-abdeltwab Jul 10, 2025
3830ef5
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Jul 10, 2025
f52adeb
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Aug 5, 2025
5628234
refactor: update the code to the last pattern
ahmed-n-abdeltwab Aug 5, 2025
34ccb6c
chore: clean up TODO after completion
ahmed-n-abdeltwab Aug 5, 2025
7db9ba2
feat: use typia for the IStats
ahmed-n-abdeltwab Aug 5, 2025
18c6218
fix: return 0 from getAppsStatistics instead of false to satisfy Typi…
ahmed-n-abdeltwab Aug 5, 2025
19affff
fix: remove the string from lastMessageSentAt to Date only to satisf…
ahmed-n-abdeltwab Aug 5, 2025
3aa34ce
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Sep 4, 2025
2748afa
Merge branch 'develop' into feat/OpenAPI
ahmed-n-abdeltwab Sep 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/odd-hounds-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fake-scope/fake-pkg": patch
---

Add OpenAPI support for the Rocket.Chat Statistics API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation.
225 changes: 201 additions & 24 deletions apps/meteor/app/api/server/v1/stats.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,129 @@
import type { TelemetryEvents } from '@rocket.chat/core-services';
import type { IStats } from '@rocket.chat/core-typings';
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';

import { getStatistics, getLastStatistics } from '../../../statistics/server';
import telemetryEvent from '../../../statistics/server/lib/telemetryEvents';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';

API.v1.addRoute(
'statistics',
{ authRequired: true },
{
async get() {
const { refresh = 'false' } = this.queryParams;
type StatisticsProps = { refresh?: 'true' | 'false' };

const StatisticsSchema = {
type: 'object',
properties: {
refresh: {
enum: ['true', 'false'],
default: 'false',
},
},
required: [],
additionalProperties: false,
};

const isStatisticsProps = ajv.compile<StatisticsProps>(StatisticsSchema);

type StatisticsListProps = {
offset: number;
count?: number;
};

const StatisticsListSchema = {
type: 'object',
properties: {
offset: {
type: 'number',
default: 0,
minimum: 0,
},
count: {
type: 'number',
default: 100,
minimum: 1,
},
},
required: [],
additionalProperties: false,
};

const isStatisticsListProps = ajv.compile<StatisticsListProps>(StatisticsListSchema);

type OTREnded = { rid: string };

type SlashCommand = { command: string };

type SettingsCounter = { settingsId: string };

type Param = {
eventName: TelemetryEvents;
timestamp?: number;
} & (OTREnded | SlashCommand | SettingsCounter);

type TelemetryPayload = {
params: Param[];
};

const statisticsEndpoints = API.v1
.get(
'statistics',
{
authRequired: true,
query: isStatisticsProps,
response: {
200: ajv.compile<IStats>({
allOf: [
{ $ref: '#/components/schemas/IStats' },
{
type: 'object',
properties: {
success: { type: 'boolean', enum: [true] },
},
required: ['success'],
},
],
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const { refresh = 'false' } = this.queryParams;
return API.v1.success(
await getLastStatistics({
userId: this.userId,
refresh: refresh === 'true',
}),
);
},
},
);

API.v1.addRoute(
'statistics.list',
{ authRequired: true },
{
async get() {
)
.get(
'statistics.list',
{
authRequired: true,
query: isStatisticsListProps,
response: {
200: ajv.compile<{ statistics: IStats[]; count: number; offset: number; total: number }>({
type: 'object',
properties: {
statistics: {
type: 'array',
items: { $ref: '#/components/schemas/IStats' },
minItems: 0,
},
count: { type: 'integer', minimum: 1 },
offset: { type: 'integer', minimum: 0, default: 0 },
total: { type: 'integer', minimum: 1 },
success: { type: 'boolean', enum: [true] },
},
additionalProperties: false,
required: ['statistics', 'count', 'offset', 'total', 'success'],
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, fields, query } = await this.parseJsonQuery();

Expand All @@ -41,14 +140,86 @@ API.v1.addRoute(
}),
);
},
},
);

API.v1.addRoute(
'statistics.telemetry',
{ authRequired: true },
{
post() {
)
.post(
'statistics.telemetry',
{
authRequired: true,
body: ajv.compile<TelemetryPayload>({
oneOf: [
{
type: 'object',
properties: {
eventName: { const: 'otrStats' },
rid: { type: 'string' },
},
required: ['eventName', 'rid'],
additionalProperties: false,
},
{
type: 'object',
properties: {
eventName: { const: 'slashCommandsStats' },
command: { type: 'string' },
},
required: ['eventName', 'command'],
additionalProperties: false,
},
{
type: 'object',
properties: {
eventName: { const: 'updateCounter' },
settingsId: { type: 'string' },
},
required: ['eventName', 'settingsId'],
additionalProperties: false,
},
],
}),
response: {
200: ajv.compile({
type: 'object',
properties: {
success: { type: 'boolean' },
},
required: ['success'],
additionalProperties: false,
}),
400: ajv.compile<{
error?: string;
errorType?: string;
stack?: string;
details?: string;
}>({
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
stack: { type: 'string' },
error: { type: 'string' },
errorType: { type: 'string' },
details: { type: 'string' },
},
required: ['success'],
additionalProperties: false,
}),
401: ajv.compile({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [false],
},
status: { type: 'string' },
message: { type: 'string' },
error: { type: 'string' },
errorType: { type: 'string' },
},
required: ['success'],
additionalProperties: false,
}),
},
},
async function action() {
const events = this.bodyParams;

events?.params?.forEach((event) => {
Expand All @@ -58,5 +229,11 @@ API.v1.addRoute(

return API.v1.success();
},
},
);
);

export type StatisticsEndpoints = ExtractRoutesFromAPI<typeof statisticsEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends StatisticsEndpoints {}
}
31 changes: 15 additions & 16 deletions apps/meteor/app/statistics/server/lib/getAppsStatistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ import { Info } from '../../../utils/rocketchat.info';

type AppsStatistics = {
engineVersion: string;
totalInstalled: number | false;
totalActive: number | false;
totalFailed: number | false;
totalPrivateApps: number | false;
totalPrivateAppsEnabled: number | false;
totalInstalled: number;
totalActive: number;
totalFailed: number;
totalPrivateApps: number;
totalPrivateAppsEnabled: number;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Numbers can behave like booleans—0 is treated as false and values greater than 0 as true. This causes confusion with the oneOf schemas generated by Typia


async function _getAppsStatistics(): Promise<AppsStatistics> {
if (!Apps.self?.isInitialized()) {
return {
engineVersion: Info.marketplaceApiVersion,
totalInstalled: false,
totalActive: false,
totalFailed: false,
totalPrivateApps: false,
totalPrivateAppsEnabled: false,
totalInstalled: 0,
totalActive: 0,
totalFailed: 0,
totalPrivateApps: 0,
totalPrivateAppsEnabled: 0,
};
}

Expand Down Expand Up @@ -58,7 +58,6 @@ async function _getAppsStatistics(): Promise<AppsStatistics> {
}
}),
);

return {
engineVersion: Info.marketplaceApiVersion,
totalInstalled,
Expand All @@ -71,11 +70,11 @@ async function _getAppsStatistics(): Promise<AppsStatistics> {
SystemLogger.error({ msg: 'Exception while getting Apps statistics', err });
return {
engineVersion: Info.marketplaceApiVersion,
totalInstalled: false,
totalActive: false,
totalFailed: false,
totalPrivateApps: false,
totalPrivateAppsEnabled: false,
totalInstalled: 0,
totalActive: 0,
totalFailed: 0,
totalPrivateApps: 0,
totalPrivateAppsEnabled: 0,
};
}
}
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,10 @@ export const statistics = {
}),
);

// TODO: the routingAlgorithm is duplicated in L202 & L227
// Type of routing algorithm used on omnichannel
statistics.routingAlgorithm = settings.get('Livechat_Routing_Method');

// TODO: the onHoldEnabled is duplicated in L205 & L230
// is on-hold active
Copy link
Contributor Author

@ahmed-n-abdeltwab ahmed-n-abdeltwab Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the onHoldEnabled, routingAlgorithm are duplicated in the stats lib

statistics.onHoldEnabled = settings.get('Livechat_allow_manual_on_hold');

Expand Down
1 change: 0 additions & 1 deletion packages/core-services/src/types/ITelemetryEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ type updateCounterDataType = { settingsId: string };
type slashCommandsDataType = { command: string };
type otrDataType = { rid: string };

// TODO this is duplicated from /packages/rest-typings/src/v1/statistics.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solved

export type TelemetryMap = { otrStats: otrDataType; slashCommandsStats: slashCommandsDataType; updateCounter: updateCounterDataType };
export type TelemetryEvents = keyof TelemetryMap;

Expand Down
4 changes: 3 additions & 1 deletion packages/core-typings/src/Ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import type { IInvite } from './IInvite';
import type { IMessage } from './IMessage';
import type { IOAuthApps } from './IOAuthApps';
import type { IStats } from './IStats';
import type { IPermission } from './IPermission';

Check failure on line 8 in packages/core-typings/src/Ajv.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

`./IPermission` type import should occur before type import of `./IStats`
import type { ISubscription } from './ISubscription';

export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission], '3.0'>();
export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IStats], '3.0'>();

Check failure on line 12 in packages/core-typings/src/Ajv.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Delete `⏎`
Loading
Loading