From a2c110a6fa7d12a5d62d8b7dc613b9eb60007454 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Mon, 21 Sep 2026 13:22:52 +0200 Subject: [PATCH] feat: bug report AI drafting extension Turns a report filed from the Admin app into a searchable issue title, a summary and steps to reproduce, instead of the reporter's raw words. Registers `bugReporter.draftIssue` as an AI capability, so it gets a row in the AI Power-Ups settings screen and a project can point it at its own model or append instructions. Decorates `IssueDrafter` rather than replacing it, because the built-in drafter is the fallback on two of the three paths through here: no model role configured, and the model call failing. A report that falls back is still complete, since the screenshots, environment table and timeline are assembled either way. Filed under 6.6.x, not 6.0.x. The extension imports `webiny/api/bug-reporter`, which only exists from 6.6.0, so an older project should fail to install it rather than end up with a project that does not build. Co-Authored-By: Claude Opus 5 (1M context) --- .../bugReportAi/BugReportDrafterDecorator.ts | 159 ++++++++++++++++++ .../6.6.x/extensions/bugReportAi/Extension.ts | 21 +++ .../6.6.x/extensions/bugReportAi/README.md | 51 ++++++ .../extensions/bugReportAi/capability.ts | 45 +++++ extensions/bug-report-ai/extension.json | 36 ++++ 5 files changed, 312 insertions(+) create mode 100644 extensions/bug-report-ai/6.6.x/extensions/bugReportAi/BugReportDrafterDecorator.ts create mode 100644 extensions/bug-report-ai/6.6.x/extensions/bugReportAi/Extension.ts create mode 100644 extensions/bug-report-ai/6.6.x/extensions/bugReportAi/README.md create mode 100644 extensions/bug-report-ai/6.6.x/extensions/bugReportAi/capability.ts create mode 100644 extensions/bug-report-ai/extension.json diff --git a/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/BugReportDrafterDecorator.ts b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/BugReportDrafterDecorator.ts new file mode 100644 index 0000000..a95ca64 --- /dev/null +++ b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/BugReportDrafterDecorator.ts @@ -0,0 +1,159 @@ +import { Output } from "ai"; +import { z } from "zod"; +import { Ai } from "webiny/api"; +import { Logger } from "webiny/api"; +import { ResolveAiCapabilityUseCase } from "webiny/api/ai-powerups"; +import { withAdditionalInstructions } from "webiny/api/ai-powerups"; +import { IssueDrafter } from "webiny/api/bug-reporter"; +import type { IBugReportPayload } from "webiny/api/bug-reporter"; +import { BUG_REPORT_DRAFT_CAPABILITY } from "./capability.js"; + +/* + * Only the first few images go to the model. They are the expensive part of the request, and a + * report with more than a handful is attaching context rather than showing the failure. + */ +const MAX_IMAGES_SENT = 3; + +/* + * Exported as the bare zod schema rather than a ready-made `Output.object(...)`: the AI SDK's + * `Output` type can't be named in emitted declarations (TS4023), so the call site wraps it. + */ +const issueDraftSchema = z.object({ + title: z.string(), + summary: z.string(), + stepsToReproduce: z.array(z.string()), + expected: z.string(), + actual: z.string() +}); + +/* Capped because a provider error message can itself be a response body. */ +const MAX_REASON_LENGTH = 200; + +function describeFailure(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + return message.slice(0, MAX_REASON_LENGTH); +} + +interface IFilePart { + type: "file"; + data: string; + mediaType: string; +} + +interface ITextPart { + type: "text"; + text: string; +} + +function buildPrompt(payload: IBugReportPayload, timeline: string): string { + const said = payload.description.trim(); + const lines: string[] = ["## What the reporter said"]; + + if (said === "") { + lines.push("_Nothing. They attached screenshots only — read them._"); + } else { + lines.push(said); + } + + lines.push( + "", + "## Where they were", + `Page: ${payload.environment.page}`, + `URL: ${payload.environment.url}`, + `Viewport: ${payload.environment.viewport}`, + `Browser: ${payload.environment.userAgent}`, + "", + "## What they did (most recent last)", + timeline + ); + + return lines.join("\n"); +} + +/* + * Screenshots go in as base64 file parts, the same way AI image enrichment sends images, and + * ahead of the text so the model has looked at them before it reads the prompt. + */ +function buildContent(payload: IBugReportPayload, prompt: string): Array { + const content: Array = []; + + for (const screenshot of payload.screenshots.slice(0, MAX_IMAGES_SENT)) { + content.push({ + type: "file", + data: screenshot.base64, + mediaType: screenshot.mediaType + }); + } + + content.push({ type: "text", text: prompt }); + + return content; +} + +/* + * Turns a filed report into a searchable title plus steps to reproduce. + * + * A decorator rather than a replacement because the base drafter is the fallback, and it is used on + * two of the three paths through here: no model role configured, and the model call failing. Only + * a successful draft discards it. Losing a report because drafting broke would be the worst + * possible trade — the reporter has already spent the effort. + */ +class BugReportDrafterDecoratorImpl implements IssueDrafter.Interface { + constructor( + private resolveCapability: ResolveAiCapabilityUseCase.Interface, + private ai: Ai.Interface, + private logger: Logger.Interface, + private decoratee: IssueDrafter.Interface + ) {} + + async execute(payload: IBugReportPayload, timeline: string): Promise { + const verbatim = await this.decoratee.execute(payload, timeline); + + const resolved = await this.resolveCapability.execute(BUG_REPORT_DRAFT_CAPABILITY); + if (resolved.isFail()) { + return verbatim; + } + + const capability = resolved.value; + const prompt = buildPrompt(payload, timeline); + + const output = Output.object({ schema: issueDraftSchema }); + const system = withAdditionalInstructions(capability); + const content = buildContent(payload, prompt); + + try { + const result = await this.ai.generateText({ + model: capability.model, + connection: capability.connection, + output, + system, + messages: [{ role: "user", content }] + }); + + return { + title: result.output.title, + summary: result.output.summary, + stepsToReproduce: result.output.stepsToReproduce, + expected: result.output.expected, + actual: result.output.actual + }; + } catch (error) { + /* + * The message only, never the error object. An AI SDK error carries + * `requestBodyValues`, and the request body here is the reporter's description plus + * the whole timeline, so logging the object would copy the report into the API logs. + * It also carries response headers, and the logger configures no redaction. + */ + this.logger.error( + { code: "BUG_REPORT_DRAFTING_FAILED", reason: describeFailure(error) }, + "Bug report drafting failed; filing verbatim." + ); + return verbatim; + } + } +} + +export const BugReportDrafterDecorator = IssueDrafter.createDecorator({ + decorator: BugReportDrafterDecoratorImpl, + dependencies: [ResolveAiCapabilityUseCase, Ai, Logger] +}); diff --git a/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/Extension.ts b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/Extension.ts new file mode 100644 index 0000000..66fd683 --- /dev/null +++ b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/Extension.ts @@ -0,0 +1,21 @@ +import { createFeature } from "webiny/api"; +import { BugReportDraftCapability } from "./capability.js"; +import { BugReportDrafterDecorator } from "./BugReportDrafterDecorator.js"; + +/** + * AI drafting for the bug reporter. + * + * An extension rather than part of the bug reporter itself: the built-in reporter files the + * reporter's own words and knows nothing about AI, so it carries no `ai`, `zod` or AI Power-Ups + * dependency. This decorates it, and deleting the extension puts the reporter back as it was. + * + * Registering the capability also puts a row in the AI Power-Ups settings screen, so the model and + * any extra instructions are configurable per project like any built-in AI feature. + */ +export default createFeature({ + name: "BugReporter/Ai", + register(container) { + container.register(BugReportDraftCapability); + container.registerDecorator(BugReportDrafterDecorator); + } +}); diff --git a/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/README.md b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/README.md new file mode 100644 index 0000000..b5dbb19 --- /dev/null +++ b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/README.md @@ -0,0 +1,51 @@ +# Bug report AI drafting + +Turns a bug report filed from the Admin app into a searchable issue title, a summary and steps to +reproduce, instead of the reporter's raw words. + +Webiny ships a bug reporter in every project: `cmd+shift+b` in the Admin app, describe what broke, +paste a screenshot, and a GitHub issue appears with the environment and a timeline of the last few +minutes attached. On its own it files what the reporter typed, verbatim. This extension decorates +that with a model call. + +## Requirements + +Webiny 6.6.0 or newer, and AI Power-Ups configured with at least one provider. + +## What it does + +`capability.ts` registers `bugReporter.draftIssue` as an AI capability, which gives it a row in the +AI Power-Ups settings screen. A project can point it at a different model or append its own +instructions there, the same as any built-in AI feature. It asks for the `standard` model role +rather than `vision`, even though it reads screenshots, because `vision` falls back to `standard` +when unset and a project that has only configured `standard` should still get drafting. + +`BugReportDrafterDecorator.ts` decorates `IssueDrafter`. It sends the reporter's words, the first +three screenshots and the recorded timeline to the model, and asks for a title, a summary, steps to +reproduce, expected and actual. + +## Why a decorator + +The built-in drafter is the fallback, and it is used on two of the three paths through here: no +model role configured, and the model call failing. Only a successful draft discards it. + +Losing a report because drafting broke would be the worst possible trade, since the reporter has +already spent the effort. Everything factual in the issue, the screenshots, the environment table +and the timeline, is assembled by the bug reporter regardless of whether a model ran, so a report +that falls back is still complete. It just reads as prose plus evidence rather than as a filled-in +form. + +Deleting this extension puts the reporter back exactly as it was. + +## Prompt + +The instructions live in `capability.ts` and are fixed. A project appends to them through the AI +Power-Ups settings screen rather than replacing them, because the output contract is part of the +implementation: the decorator parses what comes back. + +The prompt is told to work only from the evidence and never to invent a cause, a stack trace or a +step that is not in it, and to say so in the summary when the reporter was vague. The timeline is +named as the most reliable part of the input, since a failed GraphQL operation or a console error +in it usually is the bug. Reporters often paste a screenshot and type nothing, because the error +text is in the image, so it is also told to read the screenshots and quote error messages from them +exactly. diff --git a/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/capability.ts b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/capability.ts new file mode 100644 index 0000000..990c5f9 --- /dev/null +++ b/extensions/bug-report-ai/6.6.x/extensions/bugReportAi/capability.ts @@ -0,0 +1,45 @@ +import { AiCapability } from "webiny/api/ai-powerups"; + +export const BUG_REPORT_DRAFT_CAPABILITY = "bugReporter.draftIssue"; + +/* + * Fixed instructions a project can append to, not replace. The output contract lives here too: + * the decorator parses what comes back, so the shape is implementation rather than settings. + */ +const guidance = [ + "You turn a spoken or typed bug report from a Webiny team member into a GitHub issue.", + "You are given their words verbatim, any screenshots they attached, the page they were on,", + "and a timeline of what they did in the minutes before reporting. Work only from that", + "evidence.", + "", + "Write the way an engineer on the team writes: plain, specific, no filler. Never invent a", + "cause, a stack trace, or a step that is not in the evidence. If the reporter was vague and", + "nothing else fills the gap, say so in the summary rather than guessing.", + "The timeline is the most reliable part of the input — a failed GraphQL operation or a", + "console error in it usually IS the bug, so lead with it.", + "", + "Reporters often paste a screenshot and type nothing, because the error text is in the", + "image. Read the screenshots: quote error messages from them exactly, and treat what they", + "show as the reporter's description when there is no text.", + "", + "The title must be one line under 80 characters, specific enough to search for.", + "Leave stepsToReproduce empty when the evidence does not show them." +].join("\n"); + +class BugReportDraftCapabilityImpl implements AiCapability.Interface { + readonly id = BUG_REPORT_DRAFT_CAPABILITY; + readonly label = "Bug report drafting"; + readonly description = + "Turns a reported bug into an issue title, summary and steps to reproduce."; + /* + * Reads screenshots, so `vision` would fit — but `vision` falls back to `standard` when unset, + * and a project that has only configured `standard` should still get drafting. + */ + readonly defaultRole = "standard" as const; + readonly guidance = guidance; +} + +export const BugReportDraftCapability = AiCapability.createImplementation({ + implementation: BugReportDraftCapabilityImpl, + dependencies: [] +}); diff --git a/extensions/bug-report-ai/extension.json b/extensions/bug-report-ai/extension.json new file mode 100644 index 0000000..fd61b65 --- /dev/null +++ b/extensions/bug-report-ai/extension.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../schemas/extension.schema.json", + "name": "bug-report-ai", + "webinyConfigTsx": { + "imports": [ + { + "specifier": "Api", + "path": "webiny/extensions" + } + ], + "component": { + "name": "Api.Extension", + "props": { + "src": "/extensions/bugReportAi/Extension.ts" + } + } + }, + "packageJson": { + "dependencies": { + "ai": "^7.0.73", + "zod": "4.4.3" + } + }, + "additionalNotes": { + "messages": [ + { + "text": "Configure a model for %s in AI Power-Ups settings. Without one, reports are still filed, just in the reporter's own words.", + "variables": ["Bug report drafting"] + }, + { + "text": "Learn more about the extension in %s.", + "variables": ["extensions/bugReportAi/README.md"] + } + ] + } +}