diff --git a/src/api/server.debug.controller.ts b/src/api/server.debug.controller.ts index 4ab52a2c..4ca96fa7 100644 --- a/src/api/server.debug.controller.ts +++ b/src/api/server.debug.controller.ts @@ -5,10 +5,21 @@ import { Get, Logger, NotFoundException, + Query, StreamableFile, + UsePipes, } from '@nestjs/common'; import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; import { WhatsappConfigService } from '@waha/config.service'; +import { SessionManager } from '@waha/core/abc/manager.abc'; +import { WhatsappSession } from '@waha/core/abc/session.abc'; +import { + SessionApiParam, + WorkingSessionParam, +} from '@waha/nestjs/params/SessionApiParam'; +import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe'; +import { BrowserTracingQuery } from '@waha/structures/server.debug.dto'; +import { createReadStream } from 'fs'; @ApiSecurity('api_key') @Controller('api/server/debug') @@ -17,14 +28,17 @@ export class ServerDebugController { private logger: Logger; private readonly enabled: boolean; - constructor(protected config: WhatsappConfigService) { + constructor( + private config: WhatsappConfigService, + private manager: SessionManager, + ) { this.logger = new Logger('ServerDebugController'); this.enabled = this.config.debugModeEnabled; } @Get('heapsnapshot') @ApiOperation({ - summary: 'Return a heapsnapshot', + summary: 'Return a heapsnapshot for the current nodejs process', description: "Return a heapsnapshot of the server's memory", }) async heapsnapshot() { @@ -39,4 +53,27 @@ export class ServerDebugController { disposition: `attachment; filename=${fileName}`, }); } + + @Get('browser/tracing/:session') + @ApiOperation({ + summary: 'Collect and get a trace.json for Chrome DevTools ', + description: 'Uses https://pptr.dev/api/puppeteer.tracing', + }) + @SessionApiParam + @UsePipes(new WAHAValidationPipe()) + async browserTracing( + @WorkingSessionParam session: WhatsappSession, + @Query() query: BrowserTracingQuery, + ) { + if (!this.enabled) { + throw new NotFoundException('WAHA_DEBUG_MODE is disabled'); + } + const filepath = await session.browserTracing(query); + const stream = createReadStream(filepath); + const filename = `trace - ${session.name} - ${new Date()}.json`; + return new StreamableFile(stream, { + type: 'application/octet-stream', + disposition: `attachment; filename=${filename}`, + }); + } } diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index eecf37f3..cfb4e3f5 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -30,6 +30,7 @@ import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { MessageSource, WAMessage } from '@waha/structures/responses.dto'; +import { BrowserTracingQuery } from '@waha/structures/server.debug.dto'; import { DefaultMap } from '@waha/utils/DefaultMap'; import { generatePrefixedId } from '@waha/utils/ids'; import { LoggerBuilder } from '@waha/utils/logging'; @@ -349,6 +350,10 @@ export abstract class WhatsappSession { * START - Methods for API */ + public browserTracing(query: BrowserTracingQuery): Promise { + throw new NotImplementedByEngineError(); + } + /** * Auth methods */ diff --git a/src/core/engines/webjs/WPage.ts b/src/core/engines/webjs/WPage.ts index eb41822a..2a4ebb76 100644 --- a/src/core/engines/webjs/WPage.ts +++ b/src/core/engines/webjs/WPage.ts @@ -119,4 +119,8 @@ export class WPage extends EventEmitter { this.page.on(event, listener); return this.page as any; } + + get tracing() { + return this.page.tracing; + } } diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index 75b129b7..9af7c8f5 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -106,6 +106,7 @@ import { WAHAPresenceData, } from '@waha/structures/presence.dto'; import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; +import { BrowserTracingQuery } from '@waha/structures/server.debug.dto'; import { MeInfo } from '@waha/structures/sessions.dto'; import { StatusRequest, TextStatus } from '@waha/structures/status.dto'; import { @@ -117,7 +118,9 @@ import { import { PaginatorInMemory } from '@waha/utils/Paginator'; import { sleep, waitUntil } from '@waha/utils/promiseTimeout'; import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner'; +import { TmpDir } from '@waha/utils/tmpdir'; import * as lodash from 'lodash'; +import * as path from 'path'; import { ProtocolError } from 'puppeteer'; import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -519,6 +522,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { /** * START - Methods for API */ + public async browserTracing(query: BrowserTracingQuery): Promise { + const tmpdir = new TmpDir( + this.logger, + `waha-browser-tracing-${this.name}`, + (10 * query.seconds + 120) * 1000, + ); + const page = this.whatsapp.pupPage; + return await tmpdir.use(async (dir) => { + this.logger.info({ query }, `Starting browser tracing...`); + const filepath = path.join(dir, 'trace.json'); + await page.tracing.start({ path: filepath }); + await sleep(query.seconds * 1000); + await page.tracing.stop(); + this.logger.info(`Browser tracing finished, saved to ${filepath}`); + return filepath; + }); + } /** * Auth methods diff --git a/src/structures/server.debug.dto.ts b/src/structures/server.debug.dto.ts new file mode 100644 index 00000000..ac847e7a --- /dev/null +++ b/src/structures/server.debug.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class BrowserTracingQuery { + @Type(() => Number) + @IsNumber() + @ApiProperty({ + description: 'How many seconds to trace', + example: 30, + required: true, + }) + seconds: number = 30; + + @IsArray() + @IsString({ each: true }) + @Transform(({ value }) => (Array.isArray(value) ? value : [value])) + @IsOptional() + @ApiProperty({ + description: 'Categories to trace (all by default)', + example: ['*'], + required: true, + }) + categories: string[] = ['*']; +}