Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ packages/sdk-utils/test/client.js
packs
docs/

# spec-level retry bookkeeping (CI only, see PER-9011)
.cli-test-failures.json

# bstack-ai-harness:begin (managed — do not edit between markers)
bstack-ai-harness.yml
.harness-docs.json
.harness-manifest.json
CLAUDE.md
.claude/
# bstack-ai-harness:end

# spec-level retry bookkeeping (CI only, see PER-9011)
.cli-test-failures.json
5 changes: 4 additions & 1 deletion packages/client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
15 changes: 15 additions & 0 deletions packages/client/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Low] Tip emitted at WARN level

This is a promotional/informational tip, but it's logged via percy.log.warn. WARN normally signals something went wrong, and CI log processors or --fail-on-warning-style tooling may surface a benign mode-recommendation as a warning.

Suggestion: Use percy.log.info (or a dedicated tip level) unless WARN-level visibility is an explicit product requirement — in which case a one-line comment noting that intent would prevent future churn.

Reviewer: stack:code-reviewer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not applicable, it should be warning,


// instrument the recommendation; telemetry must never fail a build
try {
await percy.client.sendBuildEvents(build.id, {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Medium] Telemetry duplicates centralized pattern and omits cliVersion/clientInfo

This swallow-and-debug-log telemetry call reimplements the pattern already centralized in percy.sendCacheTelemetry (packages/core/src/percy.js:484). Unlike that path, the event body here omits cliVersion and clientInfo, which existing build events carry for attribution.

Suggestion: Route through a shared telemetry helper, or add cliVersion: percy.client.cliVersion and clientInfo: percy.clientInfo to the body to match the established event shape. At minimum, confirm with the events consumer that the slimmer payload is acceptable.

Reviewer: stack:code-reviewer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not applicable here

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 {
Expand All @@ -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);
Expand Down
104 changes: 104 additions & 0 deletions packages/core/test/snapshot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,110 @@ 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: '<html></html>',
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: '<html></html>'
});

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: '<html></html>',
enableLayout: true
});
await percy.snapshot({
name: 'snapshot two',
url: 'http://localhost:8000',
domSnapshot: '<html></html>',
enableLayout: true
});

await percy.stop();

expect(logger.stderr.filter(l => l === tip).length).toEqual(1);
});

it('still finalizes the build when recommendation telemetry fails', async () => {
percy.loglevel('debug');
spyOn(percy.client, 'sendBuildEvents').and.rejectWith(new Error('network down'));

await percy.snapshot({
name: 'test snapshot',
url: 'http://localhost:8000',
domSnapshot: '<html></html>',
enableLayout: true
});

await percy.stop();

// telemetry failure is swallowed and logged at debug; build still finalizes
expect(logger.stderr).toContain('[percy:core] VRA recommendation telemetry failed');
expect(logger.stdout).toContain(
'[percy:core] Finalized build #1: https://percy.io/test/test/123'
);
});

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: '<html></html>'
});

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 ────────────────────────────────────────────────────────
Expand Down
Loading