diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 43b21614d..c3cff616b 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -886,10 +886,13 @@ export class PercyClient { return comparison; } - async sendBuildEvents(buildId, body, meta = {}) { + async sendBuildEvents(buildId, body, meta = {}, { eventName, category } = {}) { validateId('build', buildId); this.log.debug('Sending Build Events'); return this.post(`builds/${buildId}/send-events`, { + // newer params are optional; when omitted the API applies its defaults + ...(eventName && { event_name: eventName }), + ...(category && { category }), data: body }, { identifier: 'build.send_events', ...meta }); } diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 84f0e3b0a..597a58b9e 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -2510,6 +2510,21 @@ describe('PercyClient', () => { } }); }); + + it('includes event_name and category when provided', async () => { + await expectAsync(client.sendBuildEvents(123, [ + { message: 'some event' } + ], {}, { + eventName: 'percy_cli_vra_recommendation_emitted', + category: 'percy:cli' + })).toBeResolved(); + + expect(api.requests['/builds/123/send-events'][0].body).toEqual({ + event_name: 'percy_cli_vra_recommendation_emitted', + category: 'percy:cli', + data: [{ message: 'some event' }] + }); + }); }); describe('#sendBuildLogs', () => { diff --git a/packages/core/src/snapshot.js b/packages/core/src/snapshot.js index 8e33750bf..8d23e9352 100644 --- a/packages/core/src/snapshot.js +++ b/packages/core/src/snapshot.js @@ -427,6 +427,21 @@ export function createSnapshotsQueue(percy) { percy.log.warn(`Build #${build.number} failed: ${build.url}`, { build }); await runDoctorOnFailure(percy); } else if (build?.id) { + if (build.layoutUsed) { + percy.log.warn('Tip: VRA is Percy\'s recommended visual review mode — more accurate and adaptable than Layout. Learn more: https://www.browserstack.com/docs/percy/ai-agents/visual-review-agent/overview.'); + + // instrument the recommendation; telemetry must never fail a build + try { + await percy.client.sendBuildEvents(build.id, { + message: 'VRA recommendation shown for a build using Layout review mode' + }, {}, { + eventName: 'percy_cli_vra_recommendation_emitted', + category: 'percy:cli' + }); + } catch (err) { + percy.log.debug('VRA recommendation telemetry failed', err); + } + } await percy.client.finalizeBuild(build.id); percy.log.info(`Finalized build #${build.number}: ${build.url}`, { build }); } else { @@ -444,6 +459,9 @@ export function createSnapshotsQueue(percy) { .handle('push', (snapshot, existing) => { let { name, meta } = snapshot; + // track layout usage to tip about VRA when the build is finalized + if (snapshot.enableLayout) build.layoutUsed = true; + // log immediately when not deferred or dry-running if (!percy.deferUploads) percy.log.info(`Snapshot taken: ${snapshotLogName(name, meta)}`, meta); if (percy.dryRun) percy.log.info(`Snapshot found: ${snapshotLogName(name, meta)}`, meta); diff --git a/packages/core/test/snapshot.test.js b/packages/core/test/snapshot.test.js index 93e8e281e..9496e5313 100644 --- a/packages/core/test/snapshot.test.js +++ b/packages/core/test/snapshot.test.js @@ -2194,6 +2194,90 @@ describe('Snapshot', () => { }); }); }); + + describe('VRA layout tip', () => { + let tip = '[percy] Tip: VRA is Percy\'s recommended visual review mode — more accurate and adaptable than Layout. Learn more: https://www.browserstack.com/docs/percy/ai-agents/visual-review-agent/overview.'; + + it('logs a VRA tip before finalizing when a snapshot has layout enabled', async () => { + await percy.snapshot({ + name: 'test snapshot', + url: 'http://localhost:8000', + domSnapshot: '', + enableLayout: true + }); + + await percy.stop(); + + expect(logger.stderr).toContain(tip); + expect(logger.stdout).toContain( + '[percy] Finalized build #1: https://percy.io/test/test/123' + ); + + // the recommendation is instrumented with the newer send-events params + let vraEvents = (api.requests['/builds/123/send-events'] || []) + .filter(r => r.body.event_name === 'percy_cli_vra_recommendation_emitted'); + expect(vraEvents.length).toEqual(1); + expect(vraEvents[0].body).toEqual({ + event_name: 'percy_cli_vra_recommendation_emitted', + category: 'percy:cli', + data: { + message: 'VRA recommendation shown for a build using Layout review mode' + } + }); + }); + + it('logs the VRA tip when enableLayout is set globally in config', async () => { + percy.config.snapshot.enableLayout = true; + + await percy.snapshot({ + name: 'test snapshot', + url: 'http://localhost:8000', + domSnapshot: '' + }); + + await percy.stop(); + + expect(logger.stderr).toContain(tip); + }); + + it('logs the VRA tip only once when multiple snapshots have layout enabled', async () => { + await percy.snapshot({ + name: 'snapshot one', + url: 'http://localhost:8000', + domSnapshot: '', + enableLayout: true + }); + await percy.snapshot({ + name: 'snapshot two', + url: 'http://localhost:8000', + domSnapshot: '', + enableLayout: true + }); + + await percy.stop(); + + expect(logger.stderr.filter(l => l === tip).length).toEqual(1); + }); + + it('does not log the VRA tip when no snapshot has layout enabled', async () => { + await percy.snapshot({ + name: 'test snapshot', + url: 'http://localhost:8000', + domSnapshot: '' + }); + + await percy.stop(); + + expect(logger.stderr).not.toContain(tip); + expect(logger.stdout).toContain( + '[percy] Finalized build #1: https://percy.io/test/test/123' + ); + // no recommendation event is sent when Layout is not used + let vraEvents = (api.requests['/builds/123/send-events'] || []) + .filter(r => r.body.event_name === 'percy_cli_vra_recommendation_emitted'); + expect(vraEvents.length).toEqual(0); + }); + }); }); // ── runDoctorOnFailure ────────────────────────────────────────────────────────