Skip to content

Commit bf628a6

Browse files
Merge branch 'develop' into feat/openapi-custom-user-status-list
2 parents e06a963 + 1182145 commit bf628a6

36 files changed

+469
-175
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/apps-engine': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Fixes an issue where apps logs were being lost in nested requests

apps/meteor/tests/data/apps/app-packages/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Includes pre-built app packages that are designed to test specific APIs exposed by the Apps-engine.
44

5+
DISCLAIMER: App related source code shown here are for testing purposes only, and don't reflect best practices or recommended implementation.
6+
57
## How to use
68

79
In your tests, add a `before` step and call the `installLocalTestPackage` function, passing the path of your desired package. For instance:
@@ -132,3 +134,49 @@ export class TestEndpoint extends ApiEndpoint {
132134
```
133135

134136
</details>
137+
138+
#### Nested Requests simulation
139+
140+
File name: `nested-requests_0.0.1.zip`
141+
142+
An app that simulates a "nested request" scenario. It listens for `IPostMessageSent` events, and provides a slashcommand that sends a message. Executing the slashcommand will create a scenario where the event handler for `IPostMessageSent` will be triggered by the slashcommand executor handler.
143+
144+
This situation used to cause logs for the originating handler (the slashcommand executor, in this case) to disappear, and only the logs for the nested request (`IPostMessageSent` handler, in this case) would be persisted to the database.
145+
146+
<details>
147+
<summary>App source code</summary>
148+
149+
```typescript
150+
export class NestedRequestsApp extends App implements IPostMessageSent {
151+
public async executePostMessageSent(message: IMessage, _read: IRead, _http: IHttp, _persistence: IPersistence, _modify: IModify): Promise<void> {
152+
this.getLogger().debug('executed_post_message_sent', message.id);
153+
}
154+
155+
protected async extendConfiguration(configuration: IConfigurationExtend, _environmentRead: IEnvironmentRead): Promise<void> {
156+
configuration.slashCommands.provideSlashCommand(new class implements ISlashCommand {
157+
public command= 'nest';
158+
public i18nParamsExample = 'nest';
159+
public i18nDescription = 'nest';
160+
public providesPreview = false;
161+
162+
constructor(private readonly app: IApp) { }
163+
164+
public async executor(context: SlashCommandContext, _read: IRead, modify: IModify, _http: IHttp, _persis: IPersistence): Promise<void> {
165+
const [execId] = context.getArguments();
166+
167+
this.app.getLogger().debug('slashcommand_triggered', execId);
168+
169+
const mb = modify.getCreator().startMessage()
170+
.setText(`nested_test_message ${execId}`)
171+
.setRoom(context.getRoom());
172+
173+
const id = await modify.getCreator().finish(mb);
174+
175+
this.app.getLogger().debug('slashcommand_message_sent', id);
176+
}
177+
}(this));
178+
}
179+
}
180+
```
181+
182+
</details>

apps/meteor/tests/data/apps/app-packages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ import * as path from 'path';
33
export const appImplementsIPreFileUpload = path.resolve(__dirname, './file-upload-test_0.0.1.zip');
44

55
export const appAPIParameterTest = path.resolve(__dirname, './api-parameter-test_0.0.1.zip');
6+
7+
export const appCausingNestedRequests = path.resolve(__dirname, './nested-requests_0.0.1.zip');
Binary file not shown.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { credentials, methodCall, request } from './api-data';
2+
3+
/**
4+
* Executes an app slash command via a POST request to the slashCommand method.
5+
*
6+
* @param cmd - The slashcommand name to execute
7+
* @param rid - The room ID where the command will be executed
8+
* @param params - Optional parameters to pass to the command (default: '')
9+
* @param triggerId - Optional trigger ID for the command invocation (default: 'triggerId')
10+
*
11+
* @returns Promise resolving to the API response
12+
*/
13+
export const executeAppSlashCommand = (cmd: string, rid: string, params = '', triggerId = 'triggerId') =>
14+
request
15+
.post(methodCall('slashCommand'))
16+
.set(credentials)
17+
.send({
18+
message: JSON.stringify({
19+
id: 'id',
20+
msg: 'method',
21+
method: 'slashCommand',
22+
params: [
23+
{
24+
cmd,
25+
params,
26+
triggerId,
27+
msg: {
28+
rid,
29+
_id: 'message_id',
30+
msg: `/${cmd} ${params}`.trim(),
31+
},
32+
},
33+
],
34+
}),
35+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging';
2+
import { expect } from 'chai';
3+
import { after, before, describe, it } from 'mocha';
4+
5+
import { getCredentials, request, credentials } from '../../data/api-data';
6+
import { appCausingNestedRequests } from '../../data/apps/app-packages';
7+
import { apps } from '../../data/apps/apps-data';
8+
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
9+
import { loadHistory } from '../../data/rooms.helper';
10+
import { executeAppSlashCommand } from '../../data/slashcommands.helpers';
11+
import { IS_EE } from '../../e2e/config/constants';
12+
13+
(IS_EE ? describe : describe.skip)('Apps - Logs Nested Requests', () => {
14+
before((done) => getCredentials(done));
15+
16+
after(() => cleanupApps());
17+
18+
it('should log nested requests without errors', async () => {
19+
const app = await installLocalTestPackage(appCausingNestedRequests);
20+
21+
const roomId = 'GENERAL';
22+
const execId = Math.random().toString(36).substring(2, 15);
23+
24+
const slashcommandRes = await executeAppSlashCommand('nest', roomId, execId);
25+
26+
expect(slashcommandRes.status, 'Slashcommand execution failed').to.equal(200);
27+
28+
const { messages } = await loadHistory(roomId);
29+
30+
const targetMessage = messages.find((msg) => msg.msg === `nested_test_message ${execId}`);
31+
32+
expect(targetMessage, 'Target message from nested request not found').to.exist;
33+
34+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we're checking existence above
35+
const { _id: messageId } = targetMessage!;
36+
37+
const logsRes = await request.get(apps(`/${app.id}/logs`)).set(credentials);
38+
39+
expect(logsRes.status, 'Fetching app logs failed').to.equal(200);
40+
expect(logsRes.body, 'Apps logs response success is not true').to.have.a.property('success', true);
41+
expect(logsRes.body, 'Apps logs response logs property is empty or missing')
42+
.to.have.a.property('logs')
43+
.that.is.an('array')
44+
.with.lengthOf.greaterThan(0);
45+
46+
let slashcommandExecutionLog: ILoggerStorageEntry | undefined;
47+
let postMessageExecutionLog: ILoggerStorageEntry | undefined;
48+
49+
logsRes.body.logs.forEach((log: ILoggerStorageEntry) => {
50+
if ((log.method as string) === 'slashcommand:nest:executor' && log.entries.find((entry) => entry.args.includes(messageId))) {
51+
slashcommandExecutionLog = log;
52+
return;
53+
}
54+
55+
if ((log.method as string) === 'app:executePostMessageSent' && log.entries.find((entry) => entry.args.includes(messageId))) {
56+
postMessageExecutionLog = log;
57+
}
58+
});
59+
60+
expect(postMessageExecutionLog, 'IPostMessageSent execution log not found').to.exist;
61+
62+
expect(slashcommandExecutionLog, 'Slashcommand execution log not found').to.exist;
63+
expect(
64+
slashcommandExecutionLog?.entries.filter((entry) => entry.args.includes(execId)),
65+
`Slashcommand execution log does not contain a matching execId "${execId}"`,
66+
).to.have.lengthOf(1);
67+
});
68+
});

packages/apps-engine/deno-runtime/handlers/api-handler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiE
22
import { Defined, JsonRpcError } from 'jsonrpc-lite';
33

44
import { AppObjectRegistry } from '../AppObjectRegistry.ts';
5-
import { Logger } from '../lib/logger.ts';
65
import { AppAccessorsInstance } from '../lib/accessors/mod.ts';
76
import { RequestContext } from '../lib/requestContext.ts';
7+
import { wrapComposedApp } from '../lib/wrapAppForRequest.ts';
88

99
export default async function apiHandler(request: RequestContext): Promise<JsonRpcError | Defined> {
1010
const { method: call, params } = request;
@@ -13,7 +13,7 @@ export default async function apiHandler(request: RequestContext): Promise<JsonR
1313
const path = parts.join(':');
1414

1515
const endpoint = AppObjectRegistry.get<IApiEndpoint>(`api:${path}`);
16-
const logger = AppObjectRegistry.get<Logger>('logger');
16+
const { logger } = request.context;
1717

1818
if (!endpoint) {
1919
return new JsonRpcError(`Endpoint ${path} not found`, -32000);
@@ -27,11 +27,11 @@ export default async function apiHandler(request: RequestContext): Promise<JsonR
2727

2828
const [requestData, endpointInfo] = params as Array<unknown>;
2929

30-
logger?.debug(`${path}'s ${call} is being executed...`, requestData);
30+
logger.debug(`${path}'s ${call} is being executed...`, requestData);
3131

3232
try {
3333
// deno-lint-ignore ban-types
34-
const result = await (method as Function).apply(endpoint, [
34+
const result = await (method as Function).apply(wrapComposedApp(endpoint, request), [
3535
requestData,
3636
endpointInfo,
3737
AppAccessorsInstance.getReader(),
@@ -40,11 +40,11 @@ export default async function apiHandler(request: RequestContext): Promise<JsonR
4040
AppAccessorsInstance.getPersistence(),
4141
]);
4242

43-
logger?.debug(`${path}'s ${call} was successfully executed.`);
43+
logger.debug(`${path}'s ${call} was successfully executed.`);
4444

4545
return result;
4646
} catch (e) {
47-
logger?.debug(`${path}'s ${call} was unsuccessful.`);
47+
logger.debug(`${path}'s ${call} was unsuccessful.`);
4848
return new JsonRpcError(e.message || 'Internal server error', -32000);
4949
}
5050
}

packages/apps-engine/deno-runtime/handlers/app/construct.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,8 @@ export default async function handleConstructApp(request: RequestContext): Promi
9999
// Applying the correct type here is quite difficult because of the dynamic nature of the code
100100
// deno-lint-ignore no-explicit-any
101101
const appClass = Object.values(exports)[0] as any;
102-
const logger = AppObjectRegistry.get('logger');
103102

104-
const app = new appClass(appPackage.info, logger, AppAccessorsInstance.getDefaultAppAccessors());
103+
const app = new appClass(appPackage.info, request.context.logger, AppAccessorsInstance.getDefaultAppAccessors());
105104

106105
if (typeof app.getName !== 'function') {
107106
throw new Error('App must contain a getName function');

packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts';
33
import { AppObjectRegistry } from '../../AppObjectRegistry.ts';
44
import { AppAccessorsInstance } from '../../lib/accessors/mod.ts';
55
import { RequestContext } from '../../lib/requestContext.ts';
6+
import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts';
67

7-
export default async function handleInitialize(_request: RequestContext): Promise<boolean> {
8+
export default async function handleInitialize(request: RequestContext): Promise<boolean> {
89
const app = AppObjectRegistry.get<App>('app');
910

1011
if (typeof app?.initialize !== 'function') {
@@ -13,7 +14,11 @@ export default async function handleInitialize(_request: RequestContext): Promis
1314
});
1415
}
1516

16-
await app.initialize(AppAccessorsInstance.getConfigurationExtend(), AppAccessorsInstance.getEnvironmentRead());
17+
await app.initialize.call(
18+
wrapAppForRequest(app, request),
19+
AppAccessorsInstance.getConfigurationExtend(),
20+
AppAccessorsInstance.getEnvironmentRead()
21+
);
1722

1823
return true;
1924
}

packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App.ts';
33
import { AppObjectRegistry } from '../../AppObjectRegistry.ts';
44
import { AppAccessorsInstance } from '../../lib/accessors/mod.ts';
55
import { RequestContext } from '../../lib/requestContext.ts';
6+
import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts';
67

7-
export default async function handleOnDisable(_request: RequestContext): Promise<boolean> {
8+
export default async function handleOnDisable(request: RequestContext): Promise<boolean> {
89
const app = AppObjectRegistry.get<App>('app');
910

1011
if (typeof app?.onDisable !== 'function') {
@@ -13,7 +14,7 @@ export default async function handleOnDisable(_request: RequestContext): Promise
1314
});
1415
}
1516

16-
await app.onDisable(AppAccessorsInstance.getConfigurationModify());
17+
await app.onDisable.call(wrapAppForRequest(app, request), AppAccessorsInstance.getConfigurationModify());
1718

1819
return true;
1920
}

0 commit comments

Comments
 (0)