Reporting plugins#
Status: the plugin design is human-confirmed. Implementation belongs to capture redesign #126. This contract supersedes separate extension types for harvesters, captures and providers. Those names describe responsibilities registered by one plugin type. Capture mechanisms remain reusable implementation modules.
Registration and settings#
ReportingPlugin on @snagbadger/reporting/session owns its identity, label, capability metadata, configuration-setting schema, run-setting schema, optional tab contributions and create(context). Plain objects and factory functions are supported. A plugin can register several collection/delivery capabilities. The system imports no built-ins and has no branches for known plugin IDs.
createPluginRuntime({ plugins: [{ plugin, configuration, runSettings, capabilities, enabled }] }) creates an inert initialization snapshot. Developer configuration sets bounded values, restricts capabilities, and can replace the run schema to restrict reporter edits. Plugin configuration defaults may be locked to reporters without preventing developer configuration. Reporter updates still reject unknown, locked or out-of-range values. Plugin metadata and schema trees are copied/frozen; implementation closures remain developer-owned and must obey the resolved context.
Plugin IDs and local capability IDs are nonempty and cannot contain /. Capability selections use pluginId/capabilityId. composeRegistrations from integration configuration supplies complete last-registration replacement with the first position retained; the same precedence applies to compatibility registrations. pluginCategories exposes enabled capture alternatives and the first configured default without calling a factory. Alternatives sharing a capture category cannot both be selected. enabled: false removes UI and runtime access. Per-registration capabilities restricts the allowed local IDs.
createPluginSelection(runtime.plugins) provides host-owned UI bindings. bind(id).select(localIds) chooses enabled collection capabilities; updateSettings(values) validates and stores reporter choices even before selection. snapshot() supplies qualified IDs and settings for participating plugins. The runtime validates the complete selection, snapshots settings again before activation, and never reads later UI edits. isSnapshotSelection evaluates pure capability mode metadata or a plugin's collectionMode(selected, settings) policy; it does not acquire resources.
Owned operations#
runtime.start(selection) creates one instance per participating plugin for a collection attempt. The context contains only its selected capabilities, resolved configuration and fixed run settings, cancellation, scoped custom events and public finish()/fail(error) requests. Selecting screenshots never activates an unselected video capability. A browser plugin may compose independent screenshot/recording mechanisms internally and share its own stream; the host knows neither their IDs nor their resources.
The awaited lifecycle is create → optional start → finish → dispose. ready resolves after activation; callers use completed for native/resource-originated completion. Stop is idempotent, drains accepted queued event work, validates/copies output, merges it in configured plugin order and releases instances. finish returns canonical evidence, interactions and attachment bytes. Diagnostic-only failures isolate the failing plugin. Failure in a plugin with any selected capture capability stops the entire attempt and discards its output, preserving the caller's earlier draft/evidence. Delivery failures similarly fail their operation. Cleanup errors are recorded.
Cancellation stops waiting for acquisition, handlers or finalization; late-created instances are disposed exactly once. Disposal of an already acquired instance is awaited. UI consumers can restore controls immediately while cleanup runs, but must bind all completion/publication callbacks to the owned operation rather than the mutable current one. Old commands, subscriptions and queued events cannot act on a replacement. Async plugin start hooks must cooperate with cancellation and release resources acquired after their own disposal.
Pre-recording remains explicitly authorized, diagnostic-only and default off. Both the runtime policy and a harvester capability's preRecording flag must allow it; capture capabilities cannot opt themselves into it. Registration and tab loading never authorize instrumentation.
runtime.startDelivery(qualifiedProviderId, { report, bundle }, settings) validates an owned snapshot through the portable bundle importer and rejects a report that differs from the bundle's reviewed report before creating a provider instance. Its awaited deliver(input) hook receives that owned report/bundle, including evidence from any collection plugin. runtime.deliver(...) is a receipt-returning convenience. Failure/cancellation preserves the caller's input for explicit retry. Capture instances do not survive Stop to supply private data to providers. Authentication, external idempotency and real Jira/Linear integration remain with #11.
Intermediate collection and capture adapters#
The accepted architecture follow-up #145
adds collection.collect() to generic plugin collection and an optional
PluginInstance.collect() hook. It is distinct from terminal finish():
each call returns one independently validated/copied PluginResult<CaptureOutput>
without implicitly retaining or accumulating that snapshot in the eventual report.
Delivery operations and legacy ReportingSession retain their existing interfaces.
Accepted collection requests run serially, await activation, and drain before
finalization. Stop rejects new requests immediately. Cancellation interrupts a
pending hook, drops queued collection work, discards the current snapshot and
releases resources under the existing lifetime rules. A hook must cooperate with
cancellation if its own work can outlive disposal. Calls after termination reject.
Every participating instance must supply collect; missing hooks are checked
before any hook runs. Hook, output and missing-hook failures use the collect
failure phase and the existing diagnostic-isolation/collective-capture policy.
Diagnostic failure leaves successful diagnostic output available; capture failure
discards the snapshot and terminates the attempt.
The DOM-free @snagbadger/reporting/session entry also exports
createCapturePlugin, CapturePluginOptions and CapturePluginCapture.
The factory accepts required plugin id/label, capture definitions and optional
configuration/run-setting schemas, tabs and a pure collection-mode policy.
Each definition supplies id, label, optional category/mode metadata and
load(context): CaptureAdapter. Registration is inert: only selected definitions
load and start, receiving the operation's fixed context. The reporting module
owns adapter translation, late-resource disposal, output validation and merging;
capture imports no reporting code. Existing exports and declarations remain.
const plugin = createCapturePlugin({
id: 'browser',
label: 'Browser capture',
captures: [
{
id: 'frame',
label: 'Display frame',
mode: 'snapshot',
load: () => selectedDisplayAdapter,
},
],
});
const runtime = createPluginRuntime({ plugins: [{ plugin }] });
const attempt = runtime.start({ capabilities: ['browser/frame'] });
await attempt.ready;
const snapshot = await attempt.collect();
const final = await attempt.finish();
await runtime.dispose();
Only snapshot capture adapters with repeatable: true support intermediate
collection through this bridge. The complete selected combination is checked
before any resource collects. A one-shot snapshot uses finish() instead.
Normal final collection still runs on Stop/native completion; prior intermediate
snapshots are not implicitly added to it. Schema, capability and settings policy
remain caller-supplied; the SDK embeds no study-specific permissions or controls.
Custom events#
Every operation owns a FIFO custom-event queue. context.events.on(name, handler) subscribes and returns unsubscribe; emit(name, data) returns immediately with an acceptance boolean. Events carry the publishing plugin ID. system: names are reserved: custom broadcasts cannot forge lifecycle transitions. Every plugin uses the same public lifecycle requests.
Plugins can observe system:ready after activation and system:finishing before finalization through the same subscriptions. Only the host publishes these notifications; their data identifies collection or delivery. Cancellation is observable through the context's cancellation subscription, and terminal completion through the operation's completed promise.
Stop rejects all new custom publications, including follow-up events emitted by handlers that are still draining. Such emit calls return false; hooks should finish remaining work directly instead of chaining additional events during finalization.
Handlers run serially in publication/subscription order; asynchronous rejection is attributed to the listening plugin and follows its operation's failure policy. Stop seals new publications/subscriptions and drains accepted events before finalization. Cancellation/disposal drops pending work and removes subscriptions. A queue holds at most 1,000 pending custom publications and accepts at most 10,000 custom publications per operation; exceeding a bound reports a plugin failure. This also bounds event cycles. Publishers own payload size and must supply stable data rather than mutable application objects. Plugins remain responsible for their own byte/retention limits.
Optional plugin UI#
All tab content is plugin-supplied. Recommended tab IDs are start, record and settings; they are conventions, not three plugin roles. Plugins can contribute several tabs, and matching tab IDs compose plugin-owned sections. Distinct IDs create additional tabs. The first configured contribution supplies a shared tab's label; sections stay in configured order. pluginTabs discovers every enabled contribution independently of capability selection and does not load it.
Tab loaders are separate from runtime factories and must remain inert. The optional @snagbadger/reporting/browser entry supplies mountPluginTabs and the recommended DOM PluginTabView protocol. A view mounts into its section with resolved configuration, a public selection/settings binding and shell commands; it returns cleanup. The shell supplies accessible navigation, while content remains custom. Other UI consumers can interpret their own view descriptors. Framework-independent entries import no React, CSS or browser globals.
Start, Stop, Cancel and Submit belong to the shared shell. A custom section can invoke those commands but does not own the active operation. Loading/showing a section never creates a collection instance. Failed mounting and disposal release all mounted sections, even if one cleanup throws. Late loaders cannot mount after disposal. Consumers should follow labelled controls, keyboard navigation, explicit permission choices and readable errors as recommended standards.
Maintained examples and compatibility#
The capture study registers its browser plugin once through the supported capture bridge. Its custom Start section updates the public binding; shell Start consumes that snapshot. Shared display lifetime, permission choice, per-action observation and limits derive from selected capabilities and fixed context values inside a fresh plugin instance. Retained sharing uses the same plugin operation's intermediate collection instead of intercepting adapter resources. Its Settings tab composes Browser Capture and Local Export sections. Local ZIP export creates a separate delivery instance consuming the canonical reviewed bundle. The shared-stream recording alternative is a labelled fixture, not implemented video.
createReportingRuntime, legacy adapter registration types and runtime.provider() remain an explicit compatibility boundary for existing consumers. Legacy collection registrations are translated into plugins and execute through the same coordinator; they do not have a second lifecycle implementation. New consumers should use the plugin API. Existing diagnostic mechanisms, capture factories and provider interfaces remain useful implementation building blocks, not privileged extension roles. Full launcher migration, real screen video and native picker/fidelity acceptance retain their existing owners.